summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhil Hughes <me@iamphill.com>2016-08-30 10:43:09 +0100
committerPhil Hughes <me@iamphill.com>2016-08-30 10:43:09 +0100
commit85f6244ce852fb6b788ea660c7d0cbe14ec10a20 (patch)
treeec29f6c01ea8e4ac774f3dae874a3a3abf97267b
parent2bee8e7db927d2bc2c5912b98dfe52d3c3c40fbd (diff)
parent2778dec131c2afac9fcdb2c42365b69099a5ae5b (diff)
downloadgitlab-ce-85f6244ce852fb6b788ea660c7d0cbe14ec10a20.tar.gz
Merge branch 'master' into build-cancel-spinner
-rw-r--r--.gitattributes3
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml178
-rw-r--r--.gitlab/issue_templates/Bug.md44
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md7
-rw-r--r--.hound.yml4
-rw-r--r--.mailmap35
-rw-r--r--.rubocop.yml376
-rw-r--r--.rubocop_todo.yml443
-rw-r--r--.ruby-version2
-rw-r--r--.simplecov4
-rw-r--r--.teatro.yml8
-rw-r--r--CHANGELOG447
-rw-r--r--CONTRIBUTING.md72
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile48
-rw-r--r--Gemfile.lock202
-rw-r--r--MAINTENANCE.md36
-rw-r--r--PROCESS.md2
-rw-r--r--README.md3
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/bg-header.pngbin90 -> 0 bytes
-rw-r--r--app/assets/images/bg_fallback.pngbin167 -> 0 bytes
-rw-r--r--app/assets/images/chosen-sprite.pngbin367 -> 0 bytes
-rw-r--r--app/assets/images/diff_note_add.pngbin418 -> 0 bytes
-rw-r--r--app/assets/images/emoji.pngbin1025831 -> 1087659 bytes
-rw-r--r--app/assets/images/emoji@2x.pngbin2492919 -> 2652225 bytes
-rw-r--r--app/assets/images/icon-search.pngbin222 -> 0 bytes
-rw-r--r--app/assets/images/icon_sprite.pngbin2636 -> 0 bytes
-rw-r--r--app/assets/images/images.pngbin5806 -> 0 bytes
-rw-r--r--app/assets/images/koding-logo.svg8
-rw-r--r--app/assets/images/move.pngbin197 -> 0 bytes
-rw-r--r--app/assets/images/progress_bar.gifbin494 -> 0 bytes
-rw-r--r--app/assets/images/slider_handles.pngbin1341 -> 0 bytes
-rw-r--r--app/assets/images/switch_icon.pngbin231 -> 0 bytes
-rw-r--r--app/assets/images/trans_bg.gifbin49 -> 0 bytes
-rw-r--r--app/assets/javascripts/LabelManager.js110
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee92
-rw-r--r--app/assets/javascripts/abuse_reports.js.es638
-rw-r--r--app/assets/javascripts/activities.js40
-rw-r--r--app/assets/javascripts/activities.js.coffee24
-rw-r--r--app/assets/javascripts/admin.js64
-rw-r--r--app/assets/javascripts/admin.js.coffee40
-rw-r--r--app/assets/javascripts/api.js145
-rw-r--r--app/assets/javascripts/api.js.coffee122
-rw-r--r--app/assets/javascripts/application.js329
-rw-r--r--app/assets/javascripts/application.js.coffee312
-rw-r--r--app/assets/javascripts/aside.js26
-rw-r--r--app/assets/javascripts/aside.js.coffee16
-rw-r--r--app/assets/javascripts/autosave.js63
-rw-r--r--app/assets/javascripts/autosave.js.coffee39
-rw-r--r--app/assets/javascripts/awards_handler.coffee372
-rw-r--r--app/assets/javascripts/awards_handler.js375
-rw-r--r--app/assets/javascripts/behaviors/autosize.js30
-rw-r--r--app/assets/javascripts/behaviors/autosize.js.coffee22
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.coffee15
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js15
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js58
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js.coffee56
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js45
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js.coffee52
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.coffee14
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js26
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js46
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.coffee23
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js62
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js.coffee57
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js23
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js.coffee5
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js25
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js28
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee9
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js25
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.coffee17
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee42
-rw-r--r--app/assets/javascripts/blob/template_selector.js90
-rw-r--r--app/assets/javascripts/blob/template_selector.js.coffee60
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js12
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js66
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es657
-rw-r--r--app/assets/javascripts/boards/components/board.js.es681
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es649
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es643
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es619
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es687
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es654
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es635
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es644
-rw-r--r--app/assets/javascripts/boards/models/label.js.es610
-rw-r--r--app/assets/javascripts/boards/models/list.js.es6125
-rw-r--r--app/assets/javascripts/boards/models/user.js.es68
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es661
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es6112
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js119
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es610
-rw-r--r--app/assets/javascripts/breakpoints.coffee37
-rw-r--r--app/assets/javascripts/breakpoints.js68
-rw-r--r--app/assets/javascripts/broadcast_message.js34
-rw-r--r--app/assets/javascripts/broadcast_message.js.coffee22
-rw-r--r--app/assets/javascripts/build.js164
-rw-r--r--app/assets/javascripts/build_artifacts.js27
-rw-r--r--app/assets/javascripts/build_artifacts.js.coffee14
-rw-r--r--app/assets/javascripts/ci/application.js.coffee12
-rw-r--r--app/assets/javascripts/ci/build.coffee116
-rw-r--r--app/assets/javascripts/ci/projects.js.coffee3
-rw-r--r--app/assets/javascripts/commit.js13
-rw-r--r--app/assets/javascripts/commit.js.coffee4
-rw-r--r--app/assets/javascripts/commit/file.js13
-rw-r--r--app/assets/javascripts/commit/file.js.coffee5
-rw-r--r--app/assets/javascripts/commit/image-file.js175
-rw-r--r--app/assets/javascripts/commit/image-file.js.coffee127
-rw-r--r--app/assets/javascripts/commits.js58
-rw-r--r--app/assets/javascripts/commits.js.coffee39
-rw-r--r--app/assets/javascripts/compare.js91
-rw-r--r--app/assets/javascripts/compare.js.coffee67
-rw-r--r--app/assets/javascripts/compare_autocomplete.js51
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js32
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js.coffee20
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js43
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js.coffee37
-rw-r--r--app/assets/javascripts/create_label.js.es6126
-rw-r--r--app/assets/javascripts/diff.js66
-rw-r--r--app/assets/javascripts/diff.js.coffee48
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es649
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6188
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es6107
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es618
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es660
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es635
-rw-r--r--app/assets/javascripts/diff_notes/mixins/namespace.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es687
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es688
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es653
-rw-r--r--app/assets/javascripts/dispatcher.js280
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee169
-rw-r--r--app/assets/javascripts/dropzone_input.js219
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee201
-rw-r--r--app/assets/javascripts/due_date_select.js104
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee99
-rw-r--r--app/assets/javascripts/extensions/jquery.js14
-rw-r--r--app/assets/javascripts/extensions/jquery.js.coffee11
-rw-r--r--app/assets/javascripts/files_comment_button.js146
-rw-r--r--app/assets/javascripts/flash.js43
-rw-r--r--app/assets/javascripts/flash.js.coffee24
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee228
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es6335
-rw-r--r--app/assets/javascripts/gl_dropdown.js735
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee608
-rw-r--r--app/assets/javascripts/gl_form.js53
-rw-r--r--app/assets/javascripts/gl_form.js.coffee54
-rw-r--r--app/assets/javascripts/graphs/application.js.coffee7
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js7
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js19
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js.coffee6
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js112
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js.coffee71
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js279
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee173
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js135
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee98
-rw-r--r--app/assets/javascripts/group_avatar.js21
-rw-r--r--app/assets/javascripts/group_avatar.js.coffee9
-rw-r--r--app/assets/javascripts/groups.js13
-rw-r--r--app/assets/javascripts/groups.js.coffee4
-rw-r--r--app/assets/javascripts/groups_select.js67
-rw-r--r--app/assets/javascripts/groups_select.js.coffee41
-rw-r--r--app/assets/javascripts/importer_status.js77
-rw-r--r--app/assets/javascripts/importer_status.js.coffee53
-rw-r--r--app/assets/javascripts/issuable.js86
-rw-r--r--app/assets/javascripts/issuable.js.coffee95
-rw-r--r--app/assets/javascripts/issuable_context.js69
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee60
-rw-r--r--app/assets/javascripts/issuable_form.js150
-rw-r--r--app/assets/javascripts/issuable_form.js.coffee112
-rw-r--r--app/assets/javascripts/issue.js154
-rw-r--r--app/assets/javascripts/issue.js.coffee117
-rw-r--r--app/assets/javascripts/issue_status_select.js35
-rw-r--r--app/assets/javascripts/issue_status_select.js.coffee18
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js161
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js.coffee127
-rw-r--r--app/assets/javascripts/labels.js44
-rw-r--r--app/assets/javascripts/labels.js.coffee28
-rw-r--r--app/assets/javascripts/labels_select.js365
-rw-r--r--app/assets/javascripts/labels_select.js.coffee386
-rw-r--r--app/assets/javascripts/layout_nav.js27
-rw-r--r--app/assets/javascripts/layout_nav.js.coffee24
-rw-r--r--app/assets/javascripts/lib/ace.js2
-rw-r--r--app/assets/javascripts/lib/chart.js7
-rw-r--r--app/assets/javascripts/lib/chart.js.coffee1
-rw-r--r--app/assets/javascripts/lib/cropper.js7
-rw-r--r--app/assets/javascripts/lib/cropper.js.coffee1
-rw-r--r--app/assets/javascripts/lib/d3.js7
-rw-r--r--app/assets/javascripts/lib/d3.js.coffee1
-rw-r--r--app/assets/javascripts/lib/raphael.js13
-rw-r--r--app/assets/javascripts/lib/raphael.js.coffee3
-rw-r--r--app/assets/javascripts/lib/utils/animate.js49
-rw-r--r--app/assets/javascripts/lib/utils/animate.js.coffee39
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js60
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.coffee68
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js80
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js.coffee24
-rw-r--r--app/assets/javascripts/lib/utils/md5.js211
-rw-r--r--app/assets/javascripts/lib/utils/notify.js41
-rw-r--r--app/assets/javascripts/lib/utils/notify.js.coffee35
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js115
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js.coffee105
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js15
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js.coffee9
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js74
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js.coffee52
-rw-r--r--app/assets/javascripts/lib/utils/utf8_encode.js70
-rw-r--r--app/assets/javascripts/line_highlighter.js115
-rw-r--r--app/assets/javascripts/line_highlighter.js.coffee148
-rw-r--r--app/assets/javascripts/logo.js16
-rw-r--r--app/assets/javascripts/logo.js.coffee44
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee119
-rw-r--r--app/assets/javascripts/member_expiration_date.js32
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es6341
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es683
-rw-r--r--app/assets/javascripts/merge_request.js105
-rw-r--r--app/assets/javascripts/merge_request.js.coffee82
-rw-r--r--app/assets/javascripts/merge_request_tabs.js268
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee251
-rw-r--r--app/assets/javascripts/merge_request_widget.js185
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee140
-rw-r--r--app/assets/javascripts/merged_buttons.js45
-rw-r--r--app/assets/javascripts/merged_buttons.js.coffee30
-rw-r--r--app/assets/javascripts/milestone.js195
-rw-r--r--app/assets/javascripts/milestone.js.coffee146
-rw-r--r--app/assets/javascripts/milestone_select.js155
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee137
-rw-r--r--app/assets/javascripts/namespace_select.js86
-rw-r--r--app/assets/javascripts/namespace_select.js.coffee56
-rw-r--r--app/assets/javascripts/network/application.js.coffee17
-rw-r--r--app/assets/javascripts/network/branch-graph.js404
-rw-r--r--app/assets/javascripts/network/branch-graph.js.coffee340
-rw-r--r--app/assets/javascripts/network/network.js19
-rw-r--r--app/assets/javascripts/network/network.js.coffee9
-rw-r--r--app/assets/javascripts/network/network_bundle.js16
-rw-r--r--app/assets/javascripts/new_branch_form.js104
-rw-r--r--app/assets/javascripts/new_branch_form.js.coffee78
-rw-r--r--app/assets/javascripts/new_commit_form.js34
-rw-r--r--app/assets/javascripts/new_commit_form.js.coffee21
-rw-r--r--app/assets/javascripts/notes.js810
-rw-r--r--app/assets/javascripts/notes.js.coffee694
-rw-r--r--app/assets/javascripts/notifications_dropdown.js30
-rw-r--r--app/assets/javascripts/notifications_dropdown.js.coffee25
-rw-r--r--app/assets/javascripts/notifications_form.js58
-rw-r--r--app/assets/javascripts/notifications_form.js.coffee49
-rw-r--r--app/assets/javascripts/pager.js63
-rw-r--r--app/assets/javascripts/pager.js.coffee44
-rw-r--r--app/assets/javascripts/pipeline.js.es615
-rw-r--r--app/assets/javascripts/preview_markdown.js150
-rw-r--r--app/assets/javascripts/profile/application.js.coffee2
-rw-r--r--app/assets/javascripts/profile/gl_crop.js169
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.coffee152
-rw-r--r--app/assets/javascripts/profile/profile.js102
-rw-r--r--app/assets/javascripts/profile/profile.js.coffee83
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js7
-rw-r--r--app/assets/javascripts/project.js106
-rw-r--r--app/assets/javascripts/project.js.coffee91
-rw-r--r--app/assets/javascripts/project_avatar.js21
-rw-r--r--app/assets/javascripts/project_avatar.js.coffee9
-rw-r--r--app/assets/javascripts/project_find_file.js170
-rw-r--r--app/assets/javascripts/project_find_file.js.coffee125
-rw-r--r--app/assets/javascripts/project_fork.js14
-rw-r--r--app/assets/javascripts/project_fork.js.coffee5
-rw-r--r--app/assets/javascripts/project_import.js13
-rw-r--r--app/assets/javascripts/project_import.js.coffee5
-rw-r--r--app/assets/javascripts/project_members.js10
-rw-r--r--app/assets/javascripts/project_members.js.coffee4
-rw-r--r--app/assets/javascripts/project_new.js40
-rw-r--r--app/assets/javascripts/project_new.js.coffee23
-rw-r--r--app/assets/javascripts/project_select.js102
-rw-r--r--app/assets/javascripts/project_select.js.coffee72
-rw-r--r--app/assets/javascripts/project_show.js9
-rw-r--r--app/assets/javascripts/project_show.js.coffee3
-rw-r--r--app/assets/javascripts/projects_list.js48
-rw-r--r--app/assets/javascripts/projects_list.js.coffee37
-rw-r--r--app/assets/javascripts/protected_branch_access_dropdown.js.es628
-rw-r--r--app/assets/javascripts/protected_branch_create.js.es654
-rw-r--r--app/assets/javascripts/protected_branch_dropdown.js.es675
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es666
-rw-r--r--app/assets/javascripts/protected_branch_edit_list.js.es617
-rw-r--r--app/assets/javascripts/protected_branch_select.js.coffee40
-rw-r--r--app/assets/javascripts/protected_branches.js.coffee22
-rw-r--r--app/assets/javascripts/right_sidebar.js201
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee172
-rw-r--r--app/assets/javascripts/search.js93
-rw-r--r--app/assets/javascripts/search.js.coffee75
-rw-r--r--app/assets/javascripts/search_autocomplete.js370
-rw-r--r--app/assets/javascripts/search_autocomplete.js.coffee334
-rw-r--r--app/assets/javascripts/shortcuts.js97
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee53
-rw-r--r--app/assets/javascripts/shortcuts_blob.coffee10
-rw-r--r--app/assets/javascripts/shortcuts_blob.js28
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js39
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee14
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js35
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee53
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js75
-rw-r--r--app/assets/javascripts/shortcuts_navigation.coffee23
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js64
-rw-r--r--app/assets/javascripts/shortcuts_network.js27
-rw-r--r--app/assets/javascripts/shortcuts_network.js.coffee12
-rw-r--r--app/assets/javascripts/sidebar.js41
-rw-r--r--app/assets/javascripts/sidebar.js.coffee37
-rw-r--r--app/assets/javascripts/single_file_diff.js87
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js12
-rw-r--r--app/assets/javascripts/star.js31
-rw-r--r--app/assets/javascripts/star.js.coffee24
-rw-r--r--app/assets/javascripts/subscription.js41
-rw-r--r--app/assets/javascripts/subscription.js.coffee26
-rw-r--r--app/assets/javascripts/subscription_select.js35
-rw-r--r--app/assets/javascripts/syntax_highlight.coffee20
-rw-r--r--app/assets/javascripts/syntax_highlight.js18
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es651
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es629
-rw-r--r--app/assets/javascripts/todos.js144
-rw-r--r--app/assets/javascripts/todos.js.coffee110
-rw-r--r--app/assets/javascripts/tree.js65
-rw-r--r--app/assets/javascripts/tree.js.coffee50
-rw-r--r--app/assets/javascripts/u2f/authenticate.js89
-rw-r--r--app/assets/javascripts/u2f/authenticate.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/error.js27
-rw-r--r--app/assets/javascripts/u2f/error.js.coffee13
-rw-r--r--app/assets/javascripts/u2f/register.js87
-rw-r--r--app/assets/javascripts/u2f/register.js.coffee63
-rw-r--r--app/assets/javascripts/u2f/util.js13
-rw-r--r--app/assets/javascripts/u2f/util.js.coffee.erb15
-rw-r--r--app/assets/javascripts/user.js29
-rw-r--r--app/assets/javascripts/user.js.coffee17
-rw-r--r--app/assets/javascripts/user_tabs.js119
-rw-r--r--app/assets/javascripts/user_tabs.js.coffee156
-rw-r--r--app/assets/javascripts/users/application.js.coffee2
-rw-r--r--app/assets/javascripts/users/calendar.js201
-rw-r--r--app/assets/javascripts/users/calendar.js.coffee193
-rw-r--r--app/assets/javascripts/users/users_bundle.js7
-rw-r--r--app/assets/javascripts/users_select.js355
-rw-r--r--app/assets/javascripts/users_select.js.coffee326
-rw-r--r--app/assets/javascripts/wikis.js37
-rw-r--r--app/assets/javascripts/wikis.js.coffee19
-rw-r--r--app/assets/javascripts/zen_mode.js80
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee80
-rw-r--r--app/assets/stylesheets/behaviors.scss5
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss21
-rw-r--r--app/assets/stylesheets/framework/blank.scss11
-rw-r--r--app/assets/stylesheets/framework/blocks.scss13
-rw-r--r--app/assets/stylesheets/framework/buttons.scss35
-rw-r--r--app/assets/stylesheets/framework/common.scss17
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss31
-rw-r--r--app/assets/stylesheets/framework/files.scss7
-rw-r--r--app/assets/stylesheets/framework/flash.scss22
-rw-r--r--app/assets/stylesheets/framework/forms.scss1
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/highlight.scss15
-rw-r--r--app/assets/stylesheets/framework/lists.scss11
-rw-r--r--app/assets/stylesheets/framework/logo.scss118
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss32
-rw-r--r--app/assets/stylesheets/framework/mixins.scss53
-rw-r--r--app/assets/stylesheets/framework/modal.scss1
-rw-r--r--app/assets/stylesheets/framework/nav.scss30
-rw-r--r--app/assets/stylesheets/framework/panels.scss5
-rw-r--r--app/assets/stylesheets/framework/selects.scss3
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss74
-rw-r--r--app/assets/stylesheets/framework/typography.scss42
-rw-r--r--app/assets/stylesheets/framework/variables.scss7
-rw-r--r--app/assets/stylesheets/highlight/dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss7
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white.scss13
-rw-r--r--app/assets/stylesheets/mailers/repository_push_email.scss79
-rw-r--r--app/assets/stylesheets/pages/admin.scss54
-rw-r--r--app/assets/stylesheets/pages/boards.scss306
-rw-r--r--app/assets/stylesheets/pages/builds.scss91
-rw-r--r--app/assets/stylesheets/pages/commit.scss15
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss4
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss7
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/emojis.scss2607
-rw-r--r--app/assets/stylesheets/pages/environments.scss30
-rw-r--r--app/assets/stylesheets/pages/events.scss13
-rw-r--r--app/assets/stylesheets/pages/groups.scss35
-rw-r--r--app/assets/stylesheets/pages/import.scss19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss14
-rw-r--r--app/assets/stylesheets/pages/issues.scss42
-rw-r--r--app/assets/stylesheets/pages/labels.scss19
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss238
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss61
-rw-r--r--app/assets/stylesheets/pages/note_form.scss26
-rw-r--r--app/assets/stylesheets/pages/notes.scss116
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss465
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss163
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/status.scss37
-rw-r--r--app/assets/stylesheets/pages/tags.scss7
-rw-r--r--app/assets/stylesheets/pages/todos.scss47
-rw-r--r--app/assets/stylesheets/pages/tree.scss9
-rw-r--r--app/controllers/admin/application_settings_controller.rb8
-rw-r--r--app/controllers/admin/builds_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb8
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/requests_profiles_controller.rb17
-rw-r--r--app/controllers/admin/services_controller.rb15
-rw-r--r--app/controllers/admin/spam_logs_controller.rb10
-rw-r--r--app/controllers/admin/system_info_controller.rb8
-rw-r--r--app/controllers/application_controller.rb89
-rw-r--r--app/controllers/autocomplete_controller.rb33
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb10
-rw-r--r--app/controllers/concerns/creates_commit.rb3
-rw-r--r--app/controllers/concerns/diff_for_path.rb24
-rw-r--r--app/controllers/concerns/issuable_collections.rb84
-rw-r--r--app/controllers/concerns/issues_action.rb10
-rw-r--r--app/controllers/concerns/merge_requests_action.rb10
-rw-r--r--app/controllers/concerns/service_params.rb38
-rw-r--r--app/controllers/concerns/spammable_actions.rb25
-rw-r--r--app/controllers/dashboard/todos_controller.rb20
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/help_controller.rb14
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/import/gitlab_controller.rb2
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb12
-rw-r--r--app/controllers/import/gitorious_controller.rb47
-rw-r--r--app/controllers/koding_controller.rb15
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb6
-rw-r--r--app/controllers/profiles/passwords_controller.rb1
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb15
-rw-r--r--app/controllers/profiles/u2f_registrations_controller.rb7
-rw-r--r--app/controllers/projects/application_controller.rb1
-rw-r--r--app/controllers/projects/artifacts_controller.rb5
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb21
-rw-r--r--app/controllers/projects/blob_controller.rb22
-rw-r--r--app/controllers/projects/board_lists_controller.rb65
-rw-r--r--app/controllers/projects/boards/application_controller.rb15
-rw-r--r--app/controllers/projects/boards/issues_controller.rb56
-rw-r--r--app/controllers/projects/boards/lists_controller.rb81
-rw-r--r--app/controllers/projects/boards_controller.rb15
-rw-r--r--app/controllers/projects/branches_controller.rb12
-rw-r--r--app/controllers/projects/builds_controller.rb19
-rw-r--r--app/controllers/projects/commit_controller.rb55
-rw-r--r--app/controllers/projects/compare_controller.rb45
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb20
-rw-r--r--app/controllers/projects/discussions_controller.rb43
-rw-r--r--app/controllers/projects/environments_controller.rb23
-rw-r--r--app/controllers/projects/git_http_client_controller.rb120
-rw-r--r--app/controllers/projects/git_http_controller.rb142
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb39
-rw-r--r--app/controllers/projects/lfs_api_controller.rb94
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb92
-rw-r--r--app/controllers/projects/merge_requests_controller.rb219
-rw-r--r--app/controllers/projects/notes_controller.rb99
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb36
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/controllers/projects/protected_branches_controller.rb39
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb30
-rw-r--r--app/controllers/projects/tags_controller.rb3
-rw-r--r--app/controllers/projects/templates_controller.rb19
-rw-r--r--app/controllers/projects/todos_controller.rb2
-rw-r--r--app/controllers/projects/uploads_controller.rb8
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb31
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/branches_finder.rb31
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/move_to_project_finder.rb20
-rw-r--r--app/finders/projects_finder.rb3
-rw-r--r--app/finders/todos_finder.rb32
-rw-r--r--app/helpers/appearances_helper.rb4
-rw-r--r--app/helpers/application_helper.rb13
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/avatars_helper.rb26
-rw-r--r--app/helpers/blob_helper.rb69
-rw-r--r--app/helpers/branches_helper.rb13
-rw-r--r--app/helpers/button_helper.rb22
-rw-r--r--app/helpers/ci_status_helper.rb49
-rw-r--r--app/helpers/commits_helper.rb60
-rw-r--r--app/helpers/diff_helper.rb81
-rw-r--r--app/helpers/explore_helper.rb2
-rw-r--r--app/helpers/external_wiki_helper.rb5
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/issues_helper.rb46
-rw-r--r--app/helpers/lfs_helper.rb67
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/notes_helper.rb85
-rw-r--r--app/helpers/projects_helper.rb86
-rw-r--r--app/helpers/search_helper.rb23
-rw-r--r--app/helpers/selects_helper.rb30
-rw-r--r--app/helpers/sentry_helper.rb27
-rw-r--r--app/helpers/services_helper.rb25
-rw-r--r--app/helpers/sorting_helper.rb12
-rw-r--r--app/helpers/time_helper.rb18
-rw-r--r--app/helpers/todos_helper.rb5
-rw-r--r--app/helpers/tree_helper.rb18
-rw-r--r--app/helpers/u2f_helper.rb5
-rw-r--r--app/helpers/workhorse_helper.rb6
-rw-r--r--app/mailers/emails/builds.rb1
-rw-r--r--app/mailers/emails/issues.rb5
-rw-r--r--app/mailers/emails/merge_requests.rb12
-rw-r--r--app/mailers/emails/projects.rb3
-rw-r--r--app/models/ability.rb92
-rw-r--r--app/models/application_setting.rb60
-rw-r--r--app/models/blob.rb11
-rw-r--r--app/models/board.rb7
-rw-r--r--app/models/ci/build.rb160
-rw-r--r--app/models/ci/pipeline.rb183
-rw-r--r--app/models/ci/runner.rb8
-rw-r--r--app/models/ci/trigger_request.rb8
-rw-r--r--app/models/commit.rb49
-rw-r--r--app/models/commit_status.rb60
-rw-r--r--app/models/compare.rb66
-rw-r--r--app/models/concerns/awardable.rb9
-rw-r--r--app/models/concerns/expirable.rb15
-rw-r--r--app/models/concerns/faster_cache_keys.rb16
-rw-r--r--app/models/concerns/has_status.rb93
-rw-r--r--app/models/concerns/issuable.rb27
-rw-r--r--app/models/concerns/mentionable.rb8
-rw-r--r--app/models/concerns/note_on_diff.rb29
-rw-r--r--app/models/concerns/participable.rb7
-rw-r--r--app/models/concerns/protected_branch_access.rb7
-rw-r--r--app/models/concerns/sortable.rb14
-rw-r--r--app/models/concerns/spammable.rb68
-rw-r--r--app/models/concerns/statuseable.rb81
-rw-r--r--app/models/concerns/token_authenticatable.rb30
-rw-r--r--app/models/deployment.rb10
-rw-r--r--app/models/diff_note.rb91
-rw-r--r--app/models/discussion.rb177
-rw-r--r--app/models/environment.rb18
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/issue.rb78
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/label.rb11
-rw-r--r--app/models/label_link.rb6
-rw-r--r--app/models/legacy_diff_note.rb30
-rw-r--r--app/models/list.rb34
-rw-r--r--app/models/member.rb8
-rw-r--r--app/models/members/project_member.rb15
-rw-r--r--app/models/merge_request.rb160
-rw-r--r--app/models/merge_request_diff.rb190
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/note.rb92
-rw-r--r--app/models/project.rb186
-rw-r--r--app/models/project_group_link.rb4
-rw-r--r--app/models/project_services/builds_email_service.rb26
-rw-r--r--app/models/project_services/campfire_service.rb51
-rw-r--r--app/models/project_services/hipchat_service.rb21
-rw-r--r--app/models/project_services/pivotaltracker_service.rb31
-rw-r--r--app/models/project_services/slack_service.rb51
-rw-r--r--app/models/project_team.rb92
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb9
-rw-r--r--app/models/protected_branch/merge_access_level.rb22
-rw-r--r--app/models/protected_branch/push_access_level.rb25
-rw-r--r--app/models/repository.rb229
-rw-r--r--app/models/sent_notification.rb13
-rw-r--r--app/models/service.rb34
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/spam_report.rb5
-rw-r--r--app/models/todo.rb31
-rw-r--r--app/models/u2f_registration.rb7
-rw-r--r--app/models/user.rb87
-rw-r--r--app/models/user_agent_detail.rb9
-rw-r--r--app/models/wiki_page.rb6
-rw-r--r--app/services/akismet_service.rb79
-rw-r--r--app/services/auth/container_registry_authentication_service.rb12
-rw-r--r--app/services/boards/base_service.rb5
-rw-r--r--app/services/boards/create_service.rb16
-rw-r--r--app/services/boards/issues/list_service.rb68
-rw-r--r--app/services/boards/issues/move_service.rb59
-rw-r--r--app/services/boards/lists/create_service.rb22
-rw-r--r--app/services/boards/lists/destroy_service.rb25
-rw-r--r--app/services/boards/lists/generate_service.rb36
-rw-r--r--app/services/boards/lists/move_service.rb51
-rw-r--r--app/services/ci/create_builds_service.rb56
-rw-r--r--app/services/ci/create_pipeline_builds_service.rb42
-rw-r--r--app/services/ci/create_pipeline_service.rb95
-rw-r--r--app/services/ci/create_trigger_request_service.rb17
-rw-r--r--app/services/ci/process_pipeline_service.rb77
-rw-r--r--app/services/commits/change_service.rb4
-rw-r--r--app/services/compare_service.rb8
-rw-r--r--app/services/create_branch_service.rb28
-rw-r--r--app/services/create_commit_builds_service.rb63
-rw-r--r--app/services/create_spam_log_service.rb13
-rw-r--r--app/services/delete_branch_service.rb9
-rw-r--r--app/services/delete_tag_service.rb9
-rw-r--r--app/services/delete_user_service.rb9
-rw-r--r--app/services/destroy_group_service.rb16
-rw-r--r--app/services/files/base_service.rb5
-rw-r--r--app/services/files/update_service.rb28
-rw-r--r--app/services/git_push_service.rb34
-rw-r--r--app/services/git_tag_push_service.rb26
-rw-r--r--app/services/ham_service.rb26
-rw-r--r--app/services/import_export_clean_up_service.rb24
-rw-r--r--app/services/issuable_base_service.rb107
-rw-r--r--app/services/issues/bulk_update_service.rb2
-rw-r--r--app/services/issues/close_service.rb2
-rw-r--r--app/services/issues/create_service.rb38
-rw-r--r--app/services/issues/reopen_service.rb2
-rw-r--r--app/services/issues/update_service.rb7
-rw-r--r--app/services/members/authorized_destroy_service.rb19
-rw-r--r--app/services/members/destroy_service.rb12
-rw-r--r--app/services/merge_requests/base_service.rb9
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/close_service.rb2
-rw-r--r--app/services/merge_requests/create_service.rb25
-rw-r--r--app/services/merge_requests/get_urls_service.rb63
-rw-r--r--app/services/merge_requests/merge_request_diff_cache_service.rb8
-rw-r--r--app/services/merge_requests/merge_service.rb12
-rw-r--r--app/services/merge_requests/refresh_service.rb4
-rw-r--r--app/services/merge_requests/reopen_service.rb2
-rw-r--r--app/services/merge_requests/resolve_service.rb31
-rw-r--r--app/services/merge_requests/resolved_discussion_notification_service.rb10
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/notes/create_service.rb28
-rw-r--r--app/services/notes/post_process_service.rb2
-rw-r--r--app/services/notes/slash_commands_service.rb33
-rw-r--r--app/services/notification_service.rb49
-rw-r--r--app/services/projects/autocomplete_service.rb27
-rw-r--r--app/services/projects/destroy_service.rb8
-rw-r--r--app/services/projects/enable_deploy_key_service.rb17
-rw-r--r--app/services/projects/housekeeping_service.rb26
-rw-r--r--app/services/projects/import_export/export_service.rb8
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/services/projects/participants_service.rb38
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/protected_branches/create_service.rb11
-rw-r--r--app/services/protected_branches/update_service.rb13
-rw-r--r--app/services/repository_archive_clean_up_service.rb33
-rw-r--r--app/services/slash_commands/interpret_service.rb236
-rw-r--r--app/services/spam_service.rb78
-rw-r--r--app/services/system_note_service.rb170
-rw-r--r--app/services/test_hook_service.rb2
-rw-r--r--app/services/todo_service.rb17
-rw-r--r--app/services/user_agent_detail_service.rb13
-rw-r--r--app/uploaders/artifact_uploader.rb1
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb6
-rw-r--r--app/uploaders/file_uploader.rb6
-rw-r--r--app/uploaders/lfs_object_uploader.rb2
-rw-r--r--app/uploaders/uploader_helper.rb41
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml17
-rw-r--r--app/views/admin/abuse_reports/index.html.haml33
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml87
-rw-r--r--app/views/admin/background_jobs/_head.html.haml42
-rw-r--r--app/views/admin/builds/_build.html.haml67
-rw-r--r--app/views/admin/builds/index.html.haml19
-rw-r--r--app/views/admin/dashboard/_head.html.haml54
-rw-r--r--app/views/admin/dashboard/index.html.haml6
-rw-r--r--app/views/admin/deploy_keys/new.html.haml2
-rw-r--r--app/views/admin/groups/_form.html.haml4
-rw-r--r--app/views/admin/groups/_group.html.haml40
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml3
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml2
-rw-r--r--app/views/admin/requests_profiles/index.html.haml26
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml5
-rw-r--r--app/views/admin/system_info/show.html.haml12
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/admin/users/show.html.haml8
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml87
-rw-r--r--app/views/dashboard/projects/index.html.haml3
-rw-r--r--app/views/dashboard/todos/_todo.html.haml15
-rw-r--r--app/views/dashboard/todos/index.html.haml23
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml4
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml2
-rw-r--r--app/views/discussions/_diff_discussion.html.haml6
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml17
-rw-r--r--app/views/discussions/_discussion.html.haml48
-rw-r--r--app/views/discussions/_headline.html.haml14
-rw-r--r--app/views/discussions/_jump_to_next.html.haml9
-rw-r--r--app/views/discussions/_notes.html.haml15
-rw-r--r--app/views/discussions/_parallel_diff_discussion.html.haml21
-rw-r--r--app/views/discussions/_resolve_all.html.haml11
-rw-r--r--app/views/emojis/index.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml2
-rw-r--r--app/views/events/_event.html.haml6
-rw-r--r--app/views/events/_event_scope.html.haml7
-rw-r--r--app/views/events/event/_common.html.haml9
-rw-r--r--app/views/events/event/_created_project.html.haml2
-rw-r--r--app/views/events/event/_note.html.haml11
-rw-r--r--app/views/events/event/_push.html.haml6
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/groups/edit.html.haml4
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml11
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/groups/milestones/index.html.haml1
-rw-r--r--app/views/groups/projects.html.haml1
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/help/_shortcuts.html.haml4
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/help/ui.html.haml4
-rw-r--r--app/views/import/bitbucket/status.html.haml4
-rw-r--r--app/views/import/fogbugz/status.html.haml3
-rw-r--r--app/views/import/github/new.html.haml2
-rw-r--r--app/views/import/github/status.html.haml7
-rw-r--r--app/views/import/gitlab/status.html.haml3
-rw-r--r--app/views/import/gitorious/status.html.haml55
-rw-r--r--app/views/import/google_code/status.html.haml3
-rw-r--r--app/views/koding/index.html.haml6
-rw-r--r--app/views/layouts/_collapse_button.html.haml3
-rw-r--r--app/views/layouts/_flash.html.haml2
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml10
-rw-r--r--app/views/layouts/_page.html.haml17
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml18
-rw-r--r--app/views/layouts/koding.html.haml5
-rw-r--r--app/views/layouts/nav/_admin.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml27
-rw-r--r--app/views/layouts/nav/_explore.html.haml4
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml4
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml6
-rw-r--r--app/views/layouts/project.html.haml6
-rw-r--r--app/views/notify/new_issue_email.text.erb2
-rw-r--r--app/views/notify/new_mention_in_issue_email.html.haml12
-rw-r--r--app/views/notify/new_mention_in_issue_email.text.erb7
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.html.haml15
-rw-r--r--app/views/notify/new_mention_in_merge_request_email.text.erb9
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/note_merge_request_email.html.haml2
-rw-r--r--app/views/notify/repository_push_email.html.haml3
-rw-r--r--app/views/notify/resolved_all_discussions_email.html.haml2
-rw-r--r--app/views/notify/resolved_all_discussions_email.text.erb3
-rw-r--r--app/views/profiles/_head.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml4
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml37
-rw-r--r--app/views/projects/_activity.html.haml3
-rw-r--r--app/views/projects/_bitbucket_import_modal.html.haml2
-rw-r--r--app/views/projects/_builds_settings.html.haml65
-rw-r--r--app/views/projects/_gitlab_import_modal.html.haml2
-rw-r--r--app/views/projects/_home_panel.html.haml5
-rw-r--r--app/views/projects/_merge_request_settings.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml3
-rw-r--r--app/views/projects/badges/badge.svg.erb36
-rw-r--r--app/views/projects/badges/index.html.haml23
-rw-r--r--app/views/projects/blob/_actions.html.haml3
-rw-r--r--app/views/projects/blob/_editor.html.haml6
-rw-r--r--app/views/projects/blob/_image.html.haml16
-rw-r--r--app/views/projects/blob/diff.html.haml34
-rw-r--r--app/views/projects/blob/edit.html.haml18
-rw-r--r--app/views/projects/blob/new.html.haml9
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml15
-rw-r--r--app/views/projects/boards/components/_board.html.haml43
-rw-r--r--app/views/projects/boards/components/_card.html.haml33
-rw-r--r--app/views/projects/boards/show.html.haml19
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/branches/_commit.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml40
-rw-r--r--app/views/projects/builds/_sidebar.html.haml198
-rw-r--r--app/views/projects/builds/index.html.haml21
-rw-r--r--app/views/projects/builds/show.html.haml28
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml16
-rw-r--r--app/views/projects/buttons/_koding.html.haml7
-rw-r--r--app/views/projects/buttons/_star.html.haml6
-rw-r--r--app/views/projects/ci/builds/_build.html.haml90
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml15
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml101
-rw-r--r--app/views/projects/commit/_change.html.haml4
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml8
-rw-r--r--app/views/projects/commit/_pipeline.html.haml34
-rw-r--r--app/views/projects/commit/_pipelines_list.haml17
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml5
-rw-r--r--app/views/projects/compare/_form.html.haml20
-rw-r--r--app/views/projects/compare/_ref_dropdown.html.haml4
-rw-r--r--app/views/projects/compare/show.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml22
-rw-r--r--app/views/projects/deployments/_commit.html.haml8
-rw-r--r--app/views/projects/deployments/_deployment.html.haml11
-rw-r--r--app/views/projects/diffs/_content.html.haml29
-rw-r--r--app/views/projects/diffs/_diffs.html.haml21
-rw-r--r--app/views/projects/diffs/_file.html.haml40
-rw-r--r--app/views/projects/diffs/_line.html.haml33
-rw-r--r--app/views/projects/diffs/_match_line.html.haml7
-rw-r--r--app/views/projects/diffs/_match_line_parallel.html.haml4
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml60
-rw-r--r--app/views/projects/diffs/_stats.html.haml2
-rw-r--r--app/views/projects/diffs/_text_file.html.haml22
-rw-r--r--app/views/projects/diffs/_warning.html.haml5
-rw-r--r--app/views/projects/edit.html.haml10
-rw-r--r--app/views/projects/environments/_environment.html.haml11
-rw-r--r--app/views/projects/environments/_form.html.haml29
-rw-r--r--app/views/projects/environments/edit.html.haml6
-rw-r--r--app/views/projects/environments/index.html.haml10
-rw-r--r--app/views/projects/environments/new.html.haml14
-rw-r--r--app/views/projects/environments/show.html.haml10
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/forks/new.html.haml91
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml4
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml9
-rw-r--r--app/views/projects/graphs/_head.html.haml32
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml7
-rw-r--r--app/views/projects/graphs/ci/_builds.haml7
-rw-r--r--app/views/projects/graphs/commits.html.haml4
-rw-r--r--app/views/projects/graphs/show.html.haml2
-rw-r--r--app/views/projects/group_links/index.html.haml11
-rw-r--r--app/views/projects/hooks/_project_hook.html.haml2
-rw-r--r--app/views/projects/issues/_head.html.haml51
-rw-r--r--app/views/projects/issues/_issue_by_email.html.haml27
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml4
-rw-r--r--app/views/projects/issues/index.html.haml17
-rw-r--r--app/views/projects/issues/show.html.haml13
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml3
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml13
-rw-r--r--app/views/projects/merge_requests/_show.html.haml33
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml29
-rw-r--r--app/views/projects/merge_requests/conflicts/_commit_stats.html.haml20
-rw-r--r--app/views/projects/merge_requests/conflicts/_inline_view.html.haml28
-rw-r--r--app/views/projects/merge_requests/conflicts/_parallel_view.html.haml27
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml15
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml10
-rw-r--r--app/views/projects/merge_requests/show/_pipelines.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml31
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml15
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/widget/open/_conflicts.html.haml15
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml76
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply.html.haml7
-rw-r--r--app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml25
-rw-r--r--app/views/projects/notes/_discussion.html.haml46
-rw-r--r--app/views/projects/notes/_form.html.haml10
-rw-r--r--app/views/projects/notes/_hints.html.haml13
-rw-r--r--app/views/projects/notes/_note.html.haml58
-rw-r--r--app/views/projects/notes/_notes.html.haml12
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml6
-rw-r--r--app/views/projects/notes/discussions/_diff_with_notes.html.haml17
-rw-r--r--app/views/projects/notes/discussions/_notes.html.haml6
-rw-r--r--app/views/projects/pipelines/_head.html.haml36
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml8
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_badge.html.haml27
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml80
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml11
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml34
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml41
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml12
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml10
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml10
-rw-r--r--app/views/projects/protected_branches/index.html.haml35
-rw-r--r--app/views/projects/releases/edit.html.haml16
-rw-r--r--app/views/projects/services/_form.html.haml3
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/projects/snippets/index.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml67
-rw-r--r--app/views/projects/tree/_tree_commit_column.html.haml2
-rw-r--r--app/views/projects/tree/_tree_row.html.haml6
-rw-r--r--app/views/projects/update.js.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml11
-rw-r--r--app/views/projects/wikis/_nav.html.haml22
-rw-r--r--app/views/search/results/_note.html.haml8
-rw-r--r--app/views/shared/_allow_request_access.html.haml6
-rw-r--r--app/views/shared/_import_form.html.haml2
-rw-r--r--app/views/shared/_labels_row.html.haml6
-rw-r--r--app/views/shared/_logo.svg16
-rw-r--r--app/views/shared/_nav_scroll.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml81
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/icons/_group.svg18
-rw-r--r--app/views/shared/icons/_group.svg.erb13
-rw-r--r--app/views/shared/icons/_icon_commit.svg3
-rw-r--r--app/views/shared/icons/_icon_fork.svg3
-rw-r--r--app/views/shared/icons/_icon_play.svg1
-rw-r--r--app/views/shared/icons/_icon_status_cancel.svg6
-rw-r--r--app/views/shared/icons/_icon_status_failed.svg6
-rw-r--r--app/views/shared/icons/_icon_status_pending.svg6
-rw-r--r--app/views/shared/icons/_icon_status_running.svg6
-rw-r--r--app/views/shared/icons/_icon_status_success.svg6
-rw-r--r--app/views/shared/icons/_icon_status_warning.svg6
-rw-r--r--app/views/shared/icons/_icon_timer.svg1
-rw-r--r--app/views/shared/icons/_next_discussion.svg1
-rw-r--r--app/views/shared/icons/_project.svg10
-rw-r--r--app/views/shared/icons/_project.svg.erb3
-rw-r--r--app/views/shared/issuable/_filter.html.haml23
-rw-r--r--app/views/shared/issuable/_form.html.haml40
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml12
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/members/_member.html.haml22
-rw-r--r--app/views/shared/milestones/_summary.html.haml1
-rw-r--r--app/views/shared/projects/_project.html.haml8
-rw-r--r--app/views/shared/snippets/_form.html.haml9
-rw-r--r--app/views/shared/web_hooks/_form.html.haml25
-rw-r--r--app/views/u2f/_register.html.haml13
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/email_receiver_worker.rb54
-rw-r--r--app/workers/emails_on_push_worker.rb21
-rw-r--r--app/workers/git_garbage_collect_worker.rb16
-rw-r--r--app/workers/gitlab_remove_project_export_worker.rb9
-rw-r--r--app/workers/gitlab_shell_one_shot_worker.rb10
-rw-r--r--app/workers/group_destroy_worker.rb17
-rw-r--r--app/workers/import_export_project_cleanup_worker.rb9
-rw-r--r--app/workers/irker_worker.rb6
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--app/workers/project_destroy_worker.rb2
-rw-r--r--app/workers/project_export_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb13
-rw-r--r--app/workers/repository_archive_cache_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb9
-rw-r--r--app/workers/repository_import_worker.rb6
-rw-r--r--app/workers/requests_profiles_worker.rb9
-rw-r--r--config/application.rb19
-rw-r--r--config/dependency_decisions.yml31
-rw-r--r--config/gitlab.teatro.yml87
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/1_settings.rb35
-rw-r--r--config/initializers/5_backend.rb3
-rw-r--r--config/initializers/6_validations.rb21
-rw-r--r--config/initializers/ar_monkey_patch.rb57
-rw-r--r--config/initializers/devise.rb3
-rw-r--r--config/initializers/doorkeeper.rb3
-rw-r--r--config/initializers/health_check.rb13
-rw-r--r--config/initializers/metrics.rb14
-rw-r--r--config/initializers/mime_types.rb15
-rw-r--r--config/initializers/relative_naming_ci_namespace.rb16
-rw-r--r--config/initializers/request_profiler.rb5
-rw-r--r--config/initializers/secret_token.rb103
-rw-r--r--config/initializers/session_store.rb4
-rw-r--r--config/initializers/sidekiq.rb25
-rw-r--r--config/initializers/trusted_proxies.rb12
-rw-r--r--config/mail_room.yml53
-rw-r--r--config/resque.yml.example34
-rw-r--r--config/routes.rb149
-rw-r--r--db/fixtures/development/04_project.rb1
-rw-r--r--db/fixtures/development/14_builds.rb90
-rw-r--r--db/fixtures/development/14_pipelines.rb153
-rw-r--r--db/fixtures/development/16_protected_branches.rb12
-rw-r--r--db/migrate/20140407135544_fix_namespaces.rb10
-rw-r--r--db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb8
-rw-r--r--db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb9
-rw-r--r--db/migrate/20160629025435_add_column_in_progress_merge_commit_sha_to_merge_requests.rb8
-rw-r--r--db/migrate/20160705054938_add_protected_branches_push_access.rb17
-rw-r--r--db/migrate/20160705054952_add_protected_branches_merge_access.rb17
-rw-r--r--db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb29
-rw-r--r--db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb29
-rw-r--r--db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb19
-rw-r--r--db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb19
-rw-r--r--db/migrate/20160707104333_add_lock_to_issuables.rb18
-rw-r--r--db/migrate/20160712171823_remove_award_emojis_with_no_user.rb21
-rw-r--r--db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb22
-rw-r--r--db/migrate/20160715132507_add_user_id_to_pipeline.rb7
-rw-r--r--db/migrate/20160715134306_add_index_for_pipeline_user_id.rb9
-rw-r--r--db/migrate/20160715154212_add_request_access_enabled_to_projects.rb12
-rw-r--r--db/migrate/20160715204316_add_request_access_enabled_to_groups.rb12
-rw-r--r--db/migrate/20160715230841_rename_application_settings_restricted_signup_domains.rb21
-rw-r--r--db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb8
-rw-r--r--db/migrate/20160716115711_add_queued_at_to_ci_builds.rb9
-rw-r--r--db/migrate/20160718153603_add_has_external_wiki_to_projects.rb7
-rw-r--r--db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb15
-rw-r--r--db/migrate/20160722221922_nullify_blank_type_on_notes.rb9
-rw-r--r--db/migrate/20160724205507_add_resolved_to_notes.rb10
-rw-r--r--db/migrate/20160725083350_add_external_url_to_enviroments.rb9
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb21
-rw-r--r--db/migrate/20160725104452_merge_request_diff_add_index.rb17
-rw-r--r--db/migrate/20160727163552_create_user_agent_details.rb18
-rw-r--r--db/migrate/20160727191041_create_boards.rb13
-rw-r--r--db/migrate/20160727193336_create_lists.rb16
-rw-r--r--db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb16
-rw-r--r--db/migrate/20160728103734_add_pipeline_events_to_services.rb16
-rw-r--r--db/migrate/20160729173930_remove_project_id_from_spam_logs.rb29
-rw-r--r--db/migrate/20160801163421_add_expires_at_to_member.rb29
-rw-r--r--db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb20
-rw-r--r--db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb9
-rw-r--r--db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb15
-rw-r--r--db/migrate/20160804150737_add_timestamps_to_members_again.rb21
-rw-r--r--db/migrate/20160805041956_add_deleted_at_to_namespaces.rb12
-rw-r--r--db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb27
-rw-r--r--db/migrate/20160810142633_remove_redundant_indexes.rb112
-rw-r--r--db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb29
-rw-r--r--db/migrate/20160817133006_add_koding_to_application_settings.rb10
-rw-r--r--db/migrate/20160817154936_add_discussion_ids_to_notes.rb13
-rw-r--r--db/migrate/20160818205718_add_expires_at_to_project_group_links.rb29
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb14
-rw-r--r--db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb12
-rw-r--r--db/migrate/20160823081327_change_merge_error_to_text.rb10
-rw-r--r--db/schema.rb192
-rw-r--r--doc/README.md13
-rw-r--r--doc/administration/access_restrictions.md38
-rw-r--r--doc/administration/build_artifacts.md90
-rw-r--r--doc/administration/container_registry.md7
-rw-r--r--doc/administration/custom_hooks.md3
-rw-r--r--doc/administration/high_availability/redis.md301
-rw-r--r--doc/administration/housekeeping.md2
-rw-r--r--doc/administration/img/access_restrictions.pngbin317529 -> 0 bytes
-rw-r--r--doc/administration/img/repository_storages_admin_ui.pngbin0 -> 17081 bytes
-rw-r--r--doc/administration/img/restricted_url.pngbin188210 -> 0 bytes
-rw-r--r--doc/administration/integration/koding.md242
-rw-r--r--doc/administration/raketasks/project_import_export.md15
-rw-r--r--doc/administration/repository_checks.md5
-rw-r--r--doc/administration/repository_storages.md99
-rw-r--r--doc/api/README.md15
-rw-r--r--doc/api/access_requests.md147
-rw-r--r--doc/api/award_emoji.md24
-rw-r--r--doc/api/branches.md30
-rw-r--r--doc/api/build_triggers.md8
-rw-r--r--doc/api/build_variables.md10
-rw-r--r--doc/api/builds.md102
-rw-r--r--doc/api/ci/builds.md12
-rw-r--r--doc/api/ci/runners.md4
-rw-r--r--doc/api/commits.md19
-rw-r--r--doc/api/deploy_key_multiple_projects.md10
-rw-r--r--doc/api/deploy_keys.md97
-rw-r--r--doc/api/deployments.md218
-rw-r--r--doc/api/enviroments.md117
-rw-r--r--doc/api/groups.md948
-rw-r--r--doc/api/issues.md68
-rw-r--r--doc/api/labels.md12
-rw-r--r--doc/api/licenses.md2
-rw-r--r--doc/api/members.md185
-rw-r--r--doc/api/merge_requests.md161
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/api/namespaces.md4
-rw-r--r--doc/api/notes.md6
-rw-r--r--doc/api/oauth2.md56
-rw-r--r--doc/api/pipelines.md207
-rw-r--r--doc/api/project_snippets.md3
-rw-r--r--doc/api/projects.md133
-rw-r--r--doc/api/repository_files.md16
-rw-r--r--doc/api/runners.md16
-rw-r--r--doc/api/services.md4
-rw-r--r--doc/api/session.md4
-rw-r--r--doc/api/settings.md16
-rw-r--r--doc/api/sidekiq_metrics.md8
-rw-r--r--doc/api/system_hooks.md8
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/todos.md162
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/build_artifacts/README.md177
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser.pngbin82102 -> 0 bytes
-rw-r--r--doc/ci/build_artifacts/img/build_artifacts_browser_button.pngbin7230 -> 0 bytes
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/examples/php.md4
-rw-r--r--doc/ci/permissions/README.md23
-rw-r--r--doc/ci/pipelines.md39
-rw-r--r--doc/ci/quick_start/README.md14
-rw-r--r--doc/ci/triggers/README.md22
-rw-r--r--doc/ci/variables/README.md68
-rw-r--r--doc/ci/yaml/README.md118
-rw-r--r--doc/container_registry/README.md8
-rw-r--r--doc/container_registry/img/mitmproxy-docker.pngbin0 -> 407004 bytes
-rw-r--r--doc/container_registry/troubleshooting.md141
-rw-r--r--doc/development/README.md37
-rw-r--r--doc/development/adding_database_indexes.md123
-rw-r--r--doc/development/doc_styleguide.md121
-rw-r--r--doc/development/gotchas.md5
-rw-r--r--doc/development/migration_style_guide.md46
-rw-r--r--doc/development/newlines_styleguide.md102
-rw-r--r--doc/development/performance.md9
-rw-r--r--doc/development/rake_tasks.md35
-rw-r--r--doc/development/ui_guide.md92
-rw-r--r--doc/development/what_requires_downtime.md161
-rw-r--r--doc/gitlab-basics/start-using-git.md8
-rw-r--r--doc/install/installation.md27
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/akismet.md38
-rw-r--r--doc/integration/bitbucket.md2
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/gitlab.md2
-rw-r--r--doc/integration/img/spam_log.pngbin0 -> 187190 bytes
-rw-r--r--doc/integration/img/submit_issue.pngbin0 -> 174556 bytes
-rw-r--r--doc/integration/oauth_provider.md3
-rw-r--r--doc/integration/saml.md4
-rw-r--r--doc/integration/slack.md42
-rw-r--r--doc/integration/twitter.md2
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md14
-rw-r--r--doc/markdown/markdown.md648
-rw-r--r--doc/monitoring/health_check.md6
-rw-r--r--doc/monitoring/performance/influxdb_schema.md9
-rw-r--r--doc/permissions/permissions.md103
-rw-r--r--doc/project_services/img/slack_configuration.pngbin0 -> 75762 bytes
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/project_services/slack.md50
-rw-r--r--doc/public_access/public_access.md4
-rw-r--r--doc/raketasks/backup_restore.md39
-rw-r--r--doc/raketasks/cleanup.md4
-rw-r--r--doc/raketasks/maintenance.md19
-rw-r--r--doc/raketasks/user_management.md4
-rw-r--r--doc/update/4.0-to-4.1.md2
-rw-r--r--doc/update/4.2-to-5.0.md2
-rw-r--r--doc/update/5.0-to-5.1.md2
-rw-r--r--doc/update/5.2-to-5.3.md2
-rw-r--r--doc/update/5.3-to-5.4.md2
-rw-r--r--doc/update/6.9-to-7.0.md2
-rw-r--r--doc/update/7.0-to-7.1.md2
-rw-r--r--doc/update/7.14-to-8.0.md2
-rw-r--r--doc/update/8.10-to-8.11.md191
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/user/account/security.md3
-rw-r--r--doc/user/account/two_factor_authentication.md68
-rw-r--r--doc/user/admin_area/img/admin_labels.pngbin0 -> 91459 bytes
-rw-r--r--doc/user/admin_area/labels.md9
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md20
-rw-r--r--doc/user/admin_area/settings/img/access_restrictions.pngbin0 -> 7435 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_maximum_artifacts_size.pngbin0 -> 6227 bytes
-rw-r--r--doc/user/admin_area/settings/img/admin_area_settings_button.pngbin0 -> 9184 bytes
-rw-r--r--doc/user/admin_area/settings/img/domain_blacklist.pngbin0 -> 34684 bytes
-rw-r--r--doc/user/admin_area/settings/img/restricted_url.pngbin0 -> 47539 bytes
-rw-r--r--doc/user/admin_area/settings/sign_up_restrictions.md22
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md40
-rw-r--r--doc/user/img/markdown_logo.png (renamed from doc/markdown/img/logo.png)bin9509 -> 9509 bytes
-rw-r--r--doc/user/img/markdown_video.mp4bin0 -> 383631 bytes
-rw-r--r--doc/user/markdown.md786
-rw-r--r--doc/user/permissions.md131
-rw-r--r--doc/user/project/builds/artifacts.md104
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser.pngbin0 -> 8365 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_browser_button.pngbin0 -> 11041 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_builds_page.pngbin0 -> 55625 bytes
-rw-r--r--doc/user/project/builds/img/build_artifacts_pipelines_page.pngbin0 -> 73038 bytes
-rw-r--r--doc/user/project/description_templates.md42
-rw-r--r--doc/user/project/img/description_templates.pngbin0 -> 20444 bytes
-rw-r--r--doc/user/project/img/issue_board.pngbin0 -> 275093 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin0 -> 22391 bytes
-rw-r--r--doc/user/project/img/issue_board_search_backlog.pngbin0 -> 25948 bytes
-rw-r--r--doc/user/project/img/issue_board_system_notes.pngbin0 -> 20637 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin0 -> 78694 bytes
-rw-r--r--doc/user/project/img/koding_build-in-progress.pngbin0 -> 70949 bytes
-rw-r--r--doc/user/project/img/koding_build-logs.pngbin0 -> 263623 bytes
-rw-r--r--doc/user/project/img/koding_build-success.pngbin0 -> 304666 bytes
-rw-r--r--doc/user/project/img/koding_commit-koding.yml.pngbin0 -> 302703 bytes
-rw-r--r--doc/user/project/img/koding_different-stack-on-mr-try.pngbin0 -> 333649 bytes
-rw-r--r--doc/user/project/img/koding_edit-on-ide.pngbin0 -> 330880 bytes
-rw-r--r--doc/user/project/img/koding_enable-koding.pngbin0 -> 73499 bytes
-rw-r--r--doc/user/project/img/koding_landing.pngbin0 -> 268455 bytes
-rw-r--r--doc/user/project/img/koding_open-gitlab-from-koding.pngbin0 -> 32559 bytes
-rw-r--r--doc/user/project/img/koding_run-in-ide.pngbin0 -> 65465 bytes
-rw-r--r--doc/user/project/img/koding_run-mr-in-ide.pngbin0 -> 339759 bytes
-rw-r--r--doc/user/project/img/koding_set-up-ide.pngbin0 -> 207481 bytes
-rw-r--r--doc/user/project/img/koding_stack-import.pngbin0 -> 500352 bytes
-rw-r--r--doc/user/project/img/koding_start-build.pngbin0 -> 105253 bytes
-rw-r--r--doc/user/project/img/project_settings_list.pngbin0 -> 10788 bytes
-rw-r--r--doc/user/project/img/protected_branches_choose_branch.pngbin0 -> 20659 bytes
-rw-r--r--doc/user/project/img/protected_branches_devs_can_push.pngbin0 -> 19312 bytes
-rw-r--r--doc/user/project/img/protected_branches_error_ui.pngbin0 -> 37750 bytes
-rw-r--r--doc/user/project/img/protected_branches_list.pngbin0 -> 16223 bytes
-rw-r--r--doc/user/project/img/protected_branches_matches.pngbin0 -> 32145 bytes
-rw-r--r--doc/user/project/img/protected_branches_page.pngbin0 -> 17839 bytes
-rw-r--r--doc/user/project/issue_board.md187
-rw-r--r--doc/user/project/koding.md128
-rw-r--r--doc/user/project/labels.md48
-rw-r--r--doc/user/project/merge_requests/img/conflict_section.pngbin0 -> 247537 bytes
-rw-r--r--doc/user/project/merge_requests/img/discussion_view.pngbin0 -> 292754 bytes
-rw-r--r--doc/user/project/merge_requests/img/discussions_resolved.pngbin0 -> 12840 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_widget.pngbin0 -> 32292 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_comment_button.pngbin0 -> 14075 bytes
-rw-r--r--doc/user/project/merge_requests/img/resolve_discussion_button.pngbin0 -> 18405 bytes
-rw-r--r--doc/user/project/merge_requests/merge_request_discussion_resolution.md40
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md42
-rw-r--r--doc/user/project/protected_branches.md116
-rw-r--r--doc/user/project/settings/import_export.md24
-rw-r--r--doc/user/project/slash_commands.md30
-rw-r--r--doc/web_hooks/web_hooks.md172
-rw-r--r--doc/workflow/README.md5
-rw-r--r--doc/workflow/add-user/add-user.md5
-rw-r--r--doc/workflow/award_emoji.md6
-rw-r--r--doc/workflow/cherry_pick_changes.md3
-rw-r--r--doc/workflow/file_finder.md2
-rw-r--r--doc/workflow/forking_workflow.md2
-rw-r--r--doc/workflow/gitlab_flow.md4
-rw-r--r--doc/workflow/groups.md3
-rw-r--r--doc/workflow/importing/import_projects_from_github.md3
-rw-r--r--doc/workflow/merge_requests.md29
-rw-r--r--doc/workflow/merge_requests/versions.pngbin0 -> 100566 bytes
-rw-r--r--doc/workflow/notifications.md7
-rw-r--r--doc/workflow/protected_branches.md56
-rw-r--r--doc/workflow/protected_branches/protected_branches1.pngbin195061 -> 0 bytes
-rw-r--r--doc/workflow/protected_branches/protected_branches2.pngbin41179 -> 0 bytes
-rw-r--r--doc/workflow/protected_branches/protected_branches3.pngbin110160 -> 0 bytes
-rw-r--r--doc/workflow/revert_changes.md2
-rw-r--r--doc/workflow/share_projects_with_other_groups.md18
-rw-r--r--doc/workflow/shortcuts.md73
-rw-r--r--doc/workflow/shortcuts.pngbin108209 -> 0 bytes
-rw-r--r--doc/workflow/todos.md2
-rw-r--r--doc/workflow/web_editor.md3
-rw-r--r--features/dashboard/new_project.feature2
-rw-r--r--features/explore/groups.feature25
-rw-r--r--features/project/commits/branches.feature1
-rw-r--r--features/project/commits/commits.feature5
-rw-r--r--features/project/commits/diff_comments.feature4
-rw-r--r--features/project/merge_requests.feature13
-rw-r--r--features/project/wiki.feature6
-rw-r--r--features/steps/admin/settings.rb16
-rw-r--r--features/steps/dashboard/dashboard.rb1
-rw-r--r--features/steps/dashboard/event_filters.rb13
-rw-r--r--features/steps/dashboard/help.rb2
-rw-r--r--features/steps/dashboard/issues.rb5
-rw-r--r--features/steps/dashboard/merge_requests.rb5
-rw-r--r--features/steps/dashboard/new_project.rb6
-rw-r--r--features/steps/explore/groups.rb4
-rw-r--r--features/steps/group/members.rb4
-rw-r--r--features/steps/project/badges/build.rb2
-rw-r--r--features/steps/project/builds/artifacts.rb15
-rw-r--r--features/steps/project/commits/branches.rb7
-rw-r--r--features/steps/project/commits/commits.rb19
-rw-r--r--features/steps/project/forked_merge_requests.rb3
-rw-r--r--features/steps/project/issues/award_emoji.rb2
-rw-r--r--features/steps/project/issues/issues.rb4
-rw-r--r--features/steps/project/merge_requests.rb12
-rw-r--r--features/steps/project/source/browse_files.rb12
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--features/steps/project/wiki.rb20
-rw-r--r--features/steps/shared/builds.rb8
-rw-r--r--features/steps/shared/diff_note.rb23
-rw-r--r--features/steps/shared/issuable.rb6
-rw-r--r--features/support/env.rb7
-rw-r--r--features/support/wait_for_ajax.rb11
-rw-r--r--fixtures/emojis/aliases.json191
-rw-r--r--fixtures/emojis/digests.json2166
-rw-r--r--fixtures/emojis/index.json13379
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb8
-rw-r--r--generator_templates/active_record/migration/migration.rb8
-rw-r--r--lib/api/access_requests.rb90
-rw-r--r--lib/api/api.rb30
-rw-r--r--lib/api/award_emoji.rb4
-rw-r--r--lib/api/branches.rb57
-rw-r--r--lib/api/builds.rb79
-rw-r--r--lib/api/commit_statuses.rb4
-rw-r--r--lib/api/commits.rb4
-rw-r--r--lib/api/deploy_keys.rb135
-rw-r--r--lib/api/deployments.rb40
-rw-r--r--lib/api/entities.rb148
-rw-r--r--lib/api/environments.rb83
-rw-r--r--lib/api/group_members.rb87
-rw-r--r--lib/api/helpers.rb67
-rw-r--r--lib/api/helpers/members_helpers.rb13
-rw-r--r--lib/api/internal.rb36
-rw-r--r--lib/api/issues.rb31
-rw-r--r--lib/api/members.rb158
-rw-r--r--lib/api/merge_request_diffs.rb45
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/pipelines.rb74
-rw-r--r--lib/api/project_hooks.rb4
-rw-r--r--lib/api/project_members.rb110
-rw-r--r--lib/api/projects.rb19
-rw-r--r--lib/api/session.rb1
-rw-r--r--lib/api/templates.rb26
-rw-r--r--lib/api/todos.rb8
-rw-r--r--lib/backup/files.rb2
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/backup/repository.rb12
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/autolink_filter.rb17
-rw-r--r--lib/banzai/filter/emoji_filter.rb10
-rw-r--r--lib/banzai/filter/markdown_filter.rb14
-rw-r--r--lib/banzai/filter/relative_link_filter.rb13
-rw-r--r--lib/banzai/filter/sanitization_filter.rb4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb29
-rw-r--r--lib/banzai/filter/user_reference_filter.rb10
-rw-r--r--lib/banzai/filter/video_link_filter.rb56
-rw-r--r--lib/banzai/object_renderer.rb33
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/redactor.rb37
-rw-r--r--lib/banzai/reference_extractor.rb9
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb7
-rw-r--r--lib/banzai/renderer.rb74
-rw-r--r--lib/ci/api/api.rb12
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/ci/charts.rb96
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb236
-rw-r--r--lib/ci/static_model.rb49
-rw-r--r--lib/container_registry/client.rb69
-rw-r--r--lib/container_registry/tag.rb2
-rw-r--r--lib/extracts_path.rb3
-rw-r--r--lib/gitlab/access.rb9
-rw-r--r--lib/gitlab/akismet_helper.rb47
-rw-r--r--lib/gitlab/auth.rb44
-rw-r--r--lib/gitlab/award_emoji.rb24
-rw-r--r--lib/gitlab/backend/grack_auth.rb163
-rw-r--r--lib/gitlab/backend/shell.rb10
-rw-r--r--lib/gitlab/badge/base.rb21
-rw-r--r--lib/gitlab/badge/build.rb46
-rw-r--r--lib/gitlab/badge/build/metadata.rb28
-rw-r--r--lib/gitlab/badge/build/status.rb37
-rw-r--r--lib/gitlab/badge/build/template.rb47
-rw-r--r--lib/gitlab/badge/coverage/metadata.rb30
-rw-r--r--lib/gitlab/badge/coverage/report.rb55
-rw-r--r--lib/gitlab/badge/coverage/template.rb52
-rw-r--r--lib/gitlab/badge/metadata.rb36
-rw-r--r--lib/gitlab/badge/template.rb49
-rw-r--r--lib/gitlab/build_data_builder.rb65
-rw-r--r--lib/gitlab/changes_list.rb25
-rw-r--r--lib/gitlab/checks/change_access.rb78
-rw-r--r--lib/gitlab/checks/force_push.rb17
-rw-r--r--lib/gitlab/checks/matching_merge_request.rb18
-rw-r--r--lib/gitlab/ci/config.rb2
-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/cache.rb10
-rw-r--r--lib/gitlab/ci/config/node/commands.rb33
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb25
-rw-r--r--lib/gitlab/ci/config/node/entry.rb49
-rw-r--r--lib/gitlab/ci/config/node/factory.rb47
-rw-r--r--lib/gitlab/ci/config/node/global.rb32
-rw-r--r--lib/gitlab/ci/config/node/hidden_job.rb23
-rw-r--r--lib/gitlab/ci/config/node/job.rb123
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb48
-rw-r--r--lib/gitlab/ci/config/node/legacy_validation_helpers.rb4
-rw-r--r--lib/gitlab/ci/config/node/null.rb34
-rw-r--r--lib/gitlab/ci/config/node/stage.rb22
-rw-r--r--lib/gitlab/ci/config/node/trigger.rb26
-rw-r--r--lib/gitlab/ci/config/node/undefined.rb21
-rw-r--r--lib/gitlab/ci/config/node/validatable.rb10
-rw-r--r--lib/gitlab/ci/config/node/validator.rb17
-rw-r--r--lib/gitlab/ci/config/node/validators.rb22
-rw-r--r--lib/gitlab/closing_issue_extractor.rb4
-rw-r--r--lib/gitlab/conflict/file.rb197
-rw-r--r--lib/gitlab/conflict/file_collection.rb57
-rw-r--r--lib/gitlab/conflict/parser.rb71
-rw-r--r--lib/gitlab/contributions_calendar.rb1
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/data_builder/build.rb67
-rw-r--r--lib/gitlab/data_builder/note.rb75
-rw-r--r--lib/gitlab/data_builder/pipeline.rb62
-rw-r--r--lib/gitlab/data_builder/push.rb95
-rw-r--r--lib/gitlab/database.rb6
-rw-r--r--lib/gitlab/database/migration_helpers.rb10
-rw-r--r--lib/gitlab/diff/file.rb7
-rw-r--r--lib/gitlab/diff/file_collection/base.rb35
-rw-r--r--lib/gitlab/diff/file_collection/commit.rb14
-rw-r--r--lib/gitlab/diff/file_collection/compare.rb14
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb73
-rw-r--r--lib/gitlab/diff/highlight.rb7
-rw-r--r--lib/gitlab/diff/inline_diff.rb88
-rw-r--r--lib/gitlab/diff/line.rb34
-rw-r--r--lib/gitlab/diff/parallel_diff.rb130
-rw-r--r--lib/gitlab/diff/position.rb22
-rw-r--r--lib/gitlab/downtime_check.rb71
-rw-r--r--lib/gitlab/downtime_check/message.rb39
-rw-r--r--lib/gitlab/email/handler.rb18
-rw-r--r--lib/gitlab/email/handler/base_handler.rb61
-rw-r--r--lib/gitlab/email/handler/create_issue_handler.rb52
-rw-r--r--lib/gitlab/email/handler/create_note_handler.rb55
-rw-r--r--lib/gitlab/email/message/repository_push.rb21
-rw-r--r--lib/gitlab/email/receiver.rb117
-rw-r--r--lib/gitlab/force_push_check.rb15
-rw-r--r--lib/gitlab/git.rb18
-rw-r--r--lib/gitlab/git_access.rb152
-rw-r--r--lib/gitlab/git_access_status.rb4
-rw-r--r--lib/gitlab/git_access_wiki.rb2
-rw-r--r--lib/gitlab/git_post_receive.rb1
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb4
-rw-r--r--lib/gitlab/github_import/client.rb13
-rw-r--r--lib/gitlab/github_import/hook_formatter.rb23
-rw-r--r--lib/gitlab/github_import/importer.rb158
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb26
-rw-r--r--lib/gitlab/gitlab_import/importer.rb52
-rw-r--r--lib/gitlab/gitorious_import.rb5
-rw-r--r--lib/gitlab/gitorious_import/client.rb29
-rw-r--r--lib/gitlab/gitorious_import/project_creator.rb27
-rw-r--r--lib/gitlab/gitorious_import/repository.rb35
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/highlight.rb43
-rw-r--r--lib/gitlab/import_export.rb13
-rw-r--r--lib/gitlab/import_export/avatar_restorer.rb30
-rw-r--r--lib/gitlab/import_export/avatar_saver.rb31
-rw-r--r--lib/gitlab/import_export/command_line_util.rb17
-rw-r--r--lib/gitlab/import_export/file_importer.rb18
-rw-r--r--lib/gitlab/import_export/import_export.yml28
-rw-r--r--lib/gitlab/import_export/importer.rb9
-rw-r--r--lib/gitlab/import_export/json_hash_builder.rb107
-rw-r--r--lib/gitlab/import_export/members_mapper.rb13
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb18
-rw-r--r--lib/gitlab/import_export/reader.rb77
-rw-r--r--lib/gitlab/import_export/relation_factory.rb76
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb15
-rw-r--r--lib/gitlab/import_export/repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/saver.rb5
-rw-r--r--lib/gitlab/import_export/uploads_saver.rb8
-rw-r--r--lib/gitlab/import_export/version_checker.rb2
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb1
-rw-r--r--lib/gitlab/import_sources.rb13
-rw-r--r--lib/gitlab/incoming_email.rb6
-rw-r--r--lib/gitlab/ldap/access.rb2
-rw-r--r--lib/gitlab/ldap/adapter.rb2
-rw-r--r--lib/gitlab/lfs/response.rb327
-rw-r--r--lib/gitlab/lfs/router.rb100
-rw-r--r--lib/gitlab/mail_room.rb47
-rw-r--r--lib/gitlab/metrics.rb17
-rw-r--r--lib/gitlab/metrics/instrumentation.rb13
-rw-r--r--lib/gitlab/metrics/method_call.rb4
-rw-r--r--lib/gitlab/metrics/metric.rb9
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb26
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb4
-rw-r--r--lib/gitlab/metrics/system.rb8
-rw-r--r--lib/gitlab/metrics/transaction.rb42
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb2
-rw-r--r--lib/gitlab/note_data_builder.rb73
-rw-r--r--lib/gitlab/popen.rb2
-rw-r--r--lib/gitlab/push_data_builder.rb93
-rw-r--r--lib/gitlab/redis.rb94
-rw-r--r--lib/gitlab/request_profiler.rb19
-rw-r--r--lib/gitlab/request_profiler/middleware.rb54
-rw-r--r--lib/gitlab/request_profiler/profile.rb43
-rw-r--r--lib/gitlab/sidekiq_middleware/request_store_middleware.rb13
-rw-r--r--lib/gitlab/slash_commands/command_definition.rb57
-rw-r--r--lib/gitlab/slash_commands/dsl.rb98
-rw-r--r--lib/gitlab/slash_commands/extractor.rb122
-rw-r--r--lib/gitlab/template/base_template.rb71
-rw-r--r--lib/gitlab/template/finders/base_template_finder.rb35
-rw-r--r--lib/gitlab/template/finders/global_template_finder.rb38
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb59
-rw-r--r--lib/gitlab/template/gitignore.rb22
-rw-r--r--lib/gitlab/template/gitignore_template.rb26
-rw-r--r--lib/gitlab/template/gitlab_ci_yml.rb27
-rw-r--r--lib/gitlab/template/gitlab_ci_yml_template.rb31
-rw-r--r--lib/gitlab/template/issue_template.rb19
-rw-r--r--lib/gitlab/template/merge_request_template.rb19
-rw-r--r--lib/gitlab/themes.rb16
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/user_access.rb52
-rw-r--r--lib/gitlab/utils.rb2
-rw-r--r--lib/gitlab/workhorse.rb18
-rw-r--r--lib/repository_cache.rb7
-rw-r--r--lib/rouge/formatters/html_gitlab.rb168
-rw-r--r--lib/tasks/downtime_check.rake12
-rw-r--r--lib/tasks/gemojione.rake17
-rw-r--r--lib/tasks/gitlab/bulk_add_permission.rake12
-rw-r--r--lib/tasks/gitlab/check.rake30
-rw-r--r--lib/tasks/gitlab/db.rake22
-rw-r--r--lib/tasks/gitlab/info.rake4
-rw-r--r--lib/tasks/gitlab/shell.rake12
-rw-r--r--lib/tasks/gitlab/task_helpers.rake14
-rw-r--r--lib/tasks/gitlab/track_deployment.rake9
-rw-r--r--lib/tasks/gitlab/web_hook.rake4
-rw-r--r--lib/tasks/spinach.rake8
-rw-r--r--lib/tasks/test.rake2
-rw-r--r--public/404.html84
-rw-r--r--public/422.html82
-rw-r--r--public/500.html77
-rw-r--r--public/502.html27
-rw-r--r--public/503.html27
-rw-r--r--public/deploy.html98
-rwxr-xr-xscripts/lint-doc.sh15
-rwxr-xr-xscripts/merge-simplecov30
-rwxr-xr-xscripts/prepare_build.sh7
-rw-r--r--spec/config/mail_room_spec.rb43
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb25
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb12
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb6
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb331
-rw-r--r--spec/controllers/commit_controller_spec.rb246
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/groups_controller_spec.rb30
-rw-r--r--spec/controllers/help_controller_spec.rb22
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb69
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb18
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb120
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb241
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb43
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb293
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb2
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb86
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb125
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb58
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb10
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb161
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb392
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb133
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb4
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb20
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb48
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb78
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb12
-rw-r--r--spec/controllers/projects_controller_spec.rb30
-rw-r--r--spec/factories/boards.rb5
-rw-r--r--spec/factories/broadcast_messages.rb4
-rw-r--r--spec/factories/ci/builds.rb33
-rw-r--r--spec/factories/ci/pipelines.rb20
-rw-r--r--spec/factories/ci/trigger_requests.rb3
-rw-r--r--spec/factories/commit_statuses.rb24
-rw-r--r--spec/factories/environments.rb1
-rw-r--r--spec/factories/issues.rb10
-rw-r--r--spec/factories/lists.rb20
-rw-r--r--spec/factories/project_hooks.rb11
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/factories/protected_branches.rb23
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/user_agent_details.rb7
-rw-r--r--spec/factories_spec.rb2
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb4
-rw-r--r--spec/features/admin/admin_builds_spec.rb36
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb1
-rw-r--r--spec/features/admin/admin_hooks_spec.rb6
-rw-r--r--spec/features/admin/admin_projects_spec.rb6
-rw-r--r--spec/features/admin/admin_system_info_spec.rb47
-rw-r--r--spec/features/admin/admin_users_spec.rb28
-rw-r--r--spec/features/atom/dashboard_spec.rb6
-rw-r--r--spec/features/atom/issues_spec.rb4
-rw-r--r--spec/features/atom/users_spec.rb12
-rw-r--r--spec/features/boards/boards_spec.rb634
-rw-r--r--spec/features/builds_spec.rb76
-rw-r--r--spec/features/ci_lint_spec.rb2
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/compare_spec.rb42
-rw-r--r--spec/features/dashboard/label_filter_spec.rb2
-rw-r--r--spec/features/dashboard_issues_spec.rb6
-rw-r--r--spec/features/environments_spec.rb50
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb248
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb24
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb7
-rw-r--r--spec/features/groups_spec.rb20
-rw-r--r--spec/features/help_pages_spec.rb4
-rw-r--r--spec/features/issuables/default_sort_order_spec.rb195
-rw-r--r--spec/features/issues/award_emoji_spec.rb10
-rw-r--r--spec/features/issues/award_spec.rb8
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb8
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb48
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/issues/filter_issues_spec.rb40
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb8
-rw-r--r--spec/features/issues/new_branch_button_spec.rb4
-rw-r--r--spec/features/issues/todo_spec.rb4
-rw-r--r--spec/features/issues/update_issues_spec.rb12
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb58
-rw-r--r--spec/features/issues_spec.rb77
-rw-r--r--spec/features/login_spec.rb43
-rw-r--r--spec/features/markdown_spec.rb12
-rw-r--r--spec/features/merge_requests/award_spec.rb8
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb73
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb21
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb12
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb497
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb207
-rw-r--r--spec/features/merge_requests/diffs_spec.rb25
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb13
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb37
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb2
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb48
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb33
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb32
-rw-r--r--spec/features/milestone_spec.rb4
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb37
-rw-r--r--spec/features/participants_autocomplete_spec.rb6
-rw-r--r--spec/features/pipelines_settings_spec.rb35
-rw-r--r--spec/features/pipelines_spec.rb189
-rw-r--r--spec/features/profile_spec.rb4
-rw-r--r--spec/features/profiles/password_spec.rb45
-rw-r--r--spec/features/profiles/preferences_spec.rb4
-rw-r--r--spec/features/projects/badges/coverage_spec.rb82
-rw-r--r--spec/features/projects/badges/list_spec.rb46
-rw-r--r--spec/features/projects/branches/delete_spec.rb24
-rw-r--r--spec/features/projects/branches_spec.rb32
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb31
-rw-r--r--spec/features/projects/files/editing_a_file_spec.rb34
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb29
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb5
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb3
-rw-r--r--spec/features/projects/group_links_spec.rb32
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb96
-rw-r--r--spec/features/projects/issuable_templates_spec.rb89
-rw-r--r--spec/features/projects/issues/list_spec.rb20
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb6
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb45
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb7
-rw-r--r--spec/features/projects/merge_requests/list_spec.rb20
-rw-r--r--spec/features/projects/pipelines_spec.rb262
-rw-r--r--spec/features/projects/project_settings_spec.rb41
-rw-r--r--spec/features/projects/ref_switcher_spec.rb29
-rw-r--r--spec/features/projects/slack_service/slack_service_spec.rb26
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb140
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb48
-rw-r--r--spec/features/projects_spec.rb37
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb71
-rw-r--r--spec/features/protected_branches_spec.rb14
-rw-r--r--spec/features/search_spec.rb48
-rw-r--r--spec/features/security/dashboard_access_spec.rb14
-rw-r--r--spec/features/security/project/internal_access_spec.rb19
-rw-r--r--spec/features/security/project/private_access_spec.rb19
-rw-r--r--spec/features/security/project/public_access_spec.rb19
-rw-r--r--spec/features/todos/todos_sorting_spec.rb67
-rw-r--r--spec/features/todos/todos_spec.rb23
-rw-r--r--spec/features/u2f_spec.rb119
-rw-r--r--spec/features/variables_spec.rb9
-rw-r--r--spec/finders/branches_finder_spec.rb80
-rw-r--r--spec/finders/merge_requests_finder_spec.rb4
-rw-r--r--spec/finders/move_to_project_finder_spec.rb97
-rw-r--r--spec/finders/notes_finder_spec.rb6
-rw-r--r--spec/finders/projects_finder_spec.rb73
-rw-r--r--spec/finders/todos_finder_spec.rb70
-rw-r--r--spec/fixtures/api/schemas/issue.json48
-rw-r--r--spec/fixtures/api/schemas/issues.json4
-rw-r--r--spec/fixtures/api/schemas/list.json39
-rw-r--r--spec/fixtures/api/schemas/lists.json4
-rw-r--r--spec/fixtures/config/redis_new_format_host.yml29
-rw-r--r--spec/fixtures/config/redis_new_format_socket.yml6
-rw-r--r--spec/fixtures/config/redis_old_format_host.yml5
-rw-r--r--spec/fixtures/config/redis_old_format_socket.yml3
-rw-r--r--spec/fixtures/domain_blacklist.txt3
-rw-r--r--spec/fixtures/emails/commands_in_reply.eml43
-rw-r--r--spec/fixtures/emails/commands_only_reply.eml41
-rw-r--r--spec/fixtures/emails/valid_new_issue.eml23
-rw-r--r--spec/fixtures/emails/valid_new_issue_empty.eml18
-rw-r--r--spec/fixtures/emails/wrong_authentication_token.eml18
-rw-r--r--spec/fixtures/emails/wrong_mail_key.eml (renamed from spec/fixtures/emails/wrong_reply_key.eml)0
-rw-r--r--spec/fixtures/markdown.md.erb4
-rw-r--r--spec/fixtures/parallel_diff_result.yml820
-rw-r--r--spec/fixtures/project_services/campfire/rooms.json22
-rw-r--r--spec/fixtures/project_services/campfire/rooms2.json22
-rw-r--r--spec/fixtures/video_sample.mp4bin0 -> 59122 bytes
-rw-r--r--spec/helpers/application_helper_spec.rb37
-rw-r--r--spec/helpers/blob_helper_spec.rb82
-rw-r--r--spec/helpers/ci_status_helper_spec.rb10
-rw-r--r--spec/helpers/diff_helper_spec.rb110
-rw-r--r--spec/helpers/emails_helper_spec.rb12
-rw-r--r--spec/helpers/events_helper_spec.rb16
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb20
-rw-r--r--spec/helpers/graph_helper_spec.rb2
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/issuables_helper_spec.rb16
-rw-r--r--spec/helpers/issues_helper_spec.rb117
-rw-r--r--spec/helpers/members_helper_spec.rb48
-rw-r--r--spec/helpers/notes_helper_spec.rb60
-rw-r--r--spec/helpers/page_layout_helper_spec.rb9
-rw-r--r--spec/helpers/projects_helper_spec.rb38
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/helpers/submodule_helper_spec.rb28
-rw-r--r--spec/helpers/time_helper_spec.rb26
-rw-r--r--spec/helpers/tree_helper_spec.rb4
-rw-r--r--spec/initializers/6_validations_spec.rb37
-rw-r--r--spec/initializers/secret_token_spec.rb200
-rw-r--r--spec/initializers/trusted_proxies_spec.rb12
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es641
-rw-r--r--spec/javascripts/application_spec.js32
-rw-r--r--spec/javascripts/application_spec.js.coffee30
-rw-r--r--spec/javascripts/awards_handler_spec.js261
-rw-r--r--spec/javascripts/awards_handler_spec.js.coffee200
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js21
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js.coffee11
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js93
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js.coffee70
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js44
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js.coffee49
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es6164
-rw-r--r--spec/javascripts/boards/issue_spec.js.es683
-rw-r--r--spec/javascripts/boards/list_spec.js.es689
-rw-r--r--spec/javascripts/boards/mock_data.js.es653
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee50
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es6122
-rw-r--r--spec/javascripts/extensions/array_spec.js22
-rw-r--r--spec/javascripts/extensions/array_spec.js.coffee12
-rw-r--r--spec/javascripts/extensions/jquery_spec.js42
-rw-r--r--spec/javascripts/extensions/jquery_spec.js.coffee34
-rw-r--r--spec/javascripts/fixtures/abuse_reports.html.haml16
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml2
-rw-r--r--spec/javascripts/fixtures/emoji_menu.coffee957
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js4
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml16
-rw-r--r--spec/javascripts/fixtures/issues_show.html.haml2
-rw-r--r--spec/javascripts/fixtures/projects.json2
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/issue_spec.js121
-rw-r--r--spec/javascripts/issue_spec.js.coffee109
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es689
-rw-r--r--spec/javascripts/line_highlighter_spec.js229
-rw-r--r--spec/javascripts/line_highlighter_spec.js.coffee158
-rw-r--r--spec/javascripts/merge_request_spec.js28
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee23
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js106
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js.coffee88
-rw-r--r--spec/javascripts/merge_request_widget_spec.js74
-rw-r--r--spec/javascripts/merge_request_widget_spec.js.coffee55
-rw-r--r--spec/javascripts/new_branch_spec.js170
-rw-r--r--spec/javascripts/new_branch_spec.js.coffee160
-rw-r--r--spec/javascripts/notes_spec.js41
-rw-r--r--spec/javascripts/notes_spec.js.coffee26
-rw-r--r--spec/javascripts/project_title_spec.js60
-rw-r--r--spec/javascripts/project_title_spec.js.coffee37
-rw-r--r--spec/javascripts/right_sidebar_spec.js70
-rw-r--r--spec/javascripts/right_sidebar_spec.js.coffee69
-rw-r--r--spec/javascripts/search_autocomplete_spec.js159
-rw-r--r--spec/javascripts/search_autocomplete_spec.js.coffee149
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js74
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js.coffee82
-rw-r--r--spec/javascripts/spec_helper.coffee47
-rw-r--r--spec/javascripts/spec_helper.js22
-rw-r--r--spec/javascripts/syntax_highlight_spec.js44
-rw-r--r--spec/javascripts/syntax_highlight_spec.js.coffee42
-rw-r--r--spec/javascripts/u2f/authenticate_spec.coffee52
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js75
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js33
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js.coffee15
-rw-r--r--spec/javascripts/u2f/register_spec.js81
-rw-r--r--spec/javascripts/u2f/register_spec.js.coffee57
-rw-r--r--spec/javascripts/zen_mode_spec.js73
-rw-r--r--spec/javascripts/zen_mode_spec.js.coffee51
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb20
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb110
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb64
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb34
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/video_link_filter_spec.rb50
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb44
-rw-r--r--spec/lib/banzai/redactor_spec.rb24
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb12
-rw-r--r--spec/lib/ci/charts_spec.rb20
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb230
-rw-r--r--spec/lib/container_registry/blob_spec.rb52
-rw-r--r--spec/lib/container_registry/tag_spec.rb49
-rw-r--r--spec/lib/disable_email_interceptor_spec.rb2
-rw-r--r--spec/lib/extracts_path_spec.rb34
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb4
-rw-r--r--spec/lib/gitlab/auth_spec.rb10
-rw-r--r--spec/lib/gitlab/badge/build/metadata_spec.rb27
-rw-r--r--spec/lib/gitlab/badge/build/status_spec.rb94
-rw-r--r--spec/lib/gitlab/badge/build/template_spec.rb82
-rw-r--r--spec/lib/gitlab/badge/build_spec.rb123
-rw-r--r--spec/lib/gitlab/badge/coverage/metadata_spec.rb30
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb106
-rw-r--r--spec/lib/gitlab/badge/coverage/template_spec.rb130
-rw-r--r--spec/lib/gitlab/badge/shared/metadata.rb21
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/build_data_builder_spec.rb21
-rw-r--r--spec/lib/gitlab/changes_list_spec.rb30
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb99
-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/commands_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/config/node/hidden_job_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/node/job_spec.rb86
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/node/stage_spec.rb38
-rw-r--r--spec/lib/gitlab/ci/config/node/trigger_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/config/node/validatable_spec.rb4
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb11
-rw-r--r--spec/lib/gitlab/conflict/file_collection_spec.rb24
-rw-r--r--spec/lib/gitlab/conflict/file_spec.rb261
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb193
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb21
-rw-r--r--spec/lib/gitlab/data_builder/note_spec.rb131
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb36
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb53
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb20
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb28
-rw-r--r--spec/lib/gitlab/diff/line_mapper_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/parallel_diff_spec.rb48
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb66
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb3
-rw-r--r--spec/lib/gitlab/downtime_check/message_spec.rb39
-rw-r--r--spec/lib/gitlab/downtime_check_spec.rb113
-rw-r--r--spec/lib/gitlab/email/attachment_uploader_spec.rb1
-rw-r--r--spec/lib/gitlab/email/email_shared_blocks.rb41
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb79
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb177
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb13
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb158
-rw-r--r--spec/lib/gitlab/git/hook_spec.rb1
-rw-r--r--spec/lib/gitlab/git_access_spec.rb353
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb7
-rw-r--r--spec/lib/gitlab/github_import/hook_formatter_spec.rb65
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb132
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb56
-rw-r--r--spec/lib/gitlab/gitlab_import/importer_spec.rb53
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb26
-rw-r--r--spec/lib/gitlab/highlight_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/avatar_restorer_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/avatar_saver_spec.rb27
-rw-r--r--spec/lib/gitlab/import_export/import_export_spec.rb21
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/project.json214
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb66
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb37
-rw-r--r--spec/lib/gitlab/import_export/reader_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/version_checker_spec.rb30
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb4
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb730
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb18
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb73
-rw-r--r--spec/lib/gitlab/metrics_spec.rb30
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb2
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb131
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb6
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb53
-rw-r--r--spec/lib/gitlab/redis_spec.rb79
-rw-r--r--spec/lib/gitlab/saml/user_spec.rb10
-rw-r--r--spec/lib/gitlab/search_results_spec.rb12
-rw-r--r--spec/lib/gitlab/slash_commands/command_definition_spec.rb173
-rw-r--r--spec/lib/gitlab/slash_commands/dsl_spec.rb77
-rw-r--r--spec/lib/gitlab/slash_commands/extractor_spec.rb215
-rw-r--r--spec/lib/gitlab/template/gitignore_spec.rb40
-rw-r--r--spec/lib/gitlab/template/gitignore_template_spec.rb40
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb41
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb89
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb89
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb6
-rw-r--r--spec/lib/gitlab/user_access_spec.rb138
-rw-r--r--spec/lib/repository_cache_spec.rb13
-rw-r--r--spec/mailers/emails/merge_requests_spec.rb19
-rw-r--r--spec/mailers/emails/profile_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb28
-rw-r--r--spec/models/ability_spec.rb168
-rw-r--r--spec/models/application_setting_spec.rb61
-rw-r--r--spec/models/blob_spec.rb38
-rw-r--r--spec/models/board_spec.rb12
-rw-r--r--spec/models/broadcast_message_spec.rb8
-rw-r--r--spec/models/build_spec.rb472
-rw-r--r--spec/models/ci/pipeline_spec.rb514
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb42
-rw-r--r--spec/models/commit_status_spec.rb50
-rw-r--r--spec/models/compare_spec.rb77
-rw-r--r--spec/models/concerns/faster_cache_keys_spec.rb17
-rw-r--r--spec/models/concerns/has_status_spec.rb129
-rw-r--r--spec/models/concerns/mentionable_spec.rb8
-rw-r--r--spec/models/concerns/milestoneish_spec.rb36
-rw-r--r--spec/models/concerns/spammable_spec.rb33
-rw-r--r--spec/models/concerns/statuseable_spec.rb129
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/deployment_spec.rb25
-rw-r--r--spec/models/diff_note_spec.rb302
-rw-r--r--spec/models/discussion_spec.rb615
-rw-r--r--spec/models/environment_spec.rb52
-rw-r--r--spec/models/forked_project_link_spec.rb12
-rw-r--r--spec/models/generic_commit_status_spec.rb10
-rw-r--r--spec/models/global_milestone_spec.rb18
-rw-r--r--spec/models/group_spec.rb16
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/issue_spec.rb273
-rw-r--r--spec/models/key_spec.rb9
-rw-r--r--spec/models/label_spec.rb6
-rw-r--r--spec/models/legacy_diff_note_spec.rb39
-rw-r--r--spec/models/list_spec.rb117
-rw-r--r--spec/models/member_spec.rb28
-rw-r--r--spec/models/members/group_member_spec.rb4
-rw-r--r--spec/models/members/project_member_spec.rb11
-rw-r--r--spec/models/merge_request_diff_spec.rb68
-rw-r--r--spec/models/merge_request_spec.rb347
-rw-r--r--spec/models/milestone_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb14
-rw-r--r--spec/models/network/graph_spec.rb12
-rw-r--r--spec/models/note_spec.rb141
-rw-r--r--spec/models/project_security_spec.rb20
-rw-r--r--spec/models/project_services/asana_service_spec.rb8
-rw-r--r--spec/models/project_services/assembla_service_spec.rb4
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb6
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb42
-rw-r--r--spec/models/project_services/campfire_service_spec.rb58
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb4
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb2
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb4
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb4
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb4
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb95
-rw-r--r--spec/models/project_services/irker_service_spec.rb13
-rw-r--r--spec/models/project_services/jira_service_spec.rb12
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb71
-rw-r--r--spec/models/project_services/pushover_service_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb1
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb4
-rw-r--r--spec/models/project_services/slack_service_spec.rb103
-rw-r--r--spec/models/project_spec.rb487
-rw-r--r--spec/models/project_team_spec.rb82
-rw-r--r--spec/models/repository_spec.rb277
-rw-r--r--spec/models/service_spec.rb10
-rw-r--r--spec/models/user_agent_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb181
-rw-r--r--spec/models/wiki_page_spec.rb6
-rw-r--r--spec/requests/api/access_requests_spec.rb246
-rw-r--r--spec/requests/api/api_helpers_spec.rb84
-rw-r--r--spec/requests/api/award_emoji_spec.rb38
-rw-r--r--spec/requests/api/branches_spec.rb127
-rw-r--r--spec/requests/api/builds_spec.rb173
-rw-r--r--spec/requests/api/commit_statuses_spec.rb10
-rw-r--r--spec/requests/api/commits_spec.rb63
-rw-r--r--spec/requests/api/deploy_keys_spec.rb160
-rw-r--r--spec/requests/api/deployments_spec.rb60
-rw-r--r--spec/requests/api/environments_spec.rb130
-rw-r--r--spec/requests/api/files_spec.rb22
-rw-r--r--spec/requests/api/fork_spec.rb12
-rw-r--r--spec/requests/api/group_members_spec.rb199
-rw-r--r--spec/requests/api/groups_spec.rb56
-rw-r--r--spec/requests/api/internal_spec.rb94
-rw-r--r--spec/requests/api/issues_spec.rb137
-rw-r--r--spec/requests/api/keys_spec.rb6
-rw-r--r--spec/requests/api/labels_spec.rb60
-rw-r--r--spec/requests/api/members_spec.rb314
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb49
-rw-r--r--spec/requests/api/merge_requests_spec.rb93
-rw-r--r--spec/requests/api/milestones_spec.rb30
-rw-r--r--spec/requests/api/namespaces_spec.rb10
-rw-r--r--spec/requests/api/notes_spec.rb58
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb33
-rw-r--r--spec/requests/api/pipelines_spec.rb133
-rw-r--r--spec/requests/api/project_hooks_spec.rb52
-rw-r--r--spec/requests/api/project_members_spec.rb166
-rw-r--r--spec/requests/api/project_snippets_spec.rb3
-rw-r--r--spec/requests/api/projects_spec.rb287
-rw-r--r--spec/requests/api/repositories_spec.rb40
-rw-r--r--spec/requests/api/runners_spec.rb98
-rw-r--r--spec/requests/api/services_spec.rb22
-rw-r--r--spec/requests/api/session_spec.rb23
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/api/system_hooks_spec.rb20
-rw-r--r--spec/requests/api/tags_spec.rb34
-rw-r--r--spec/requests/api/templates_spec.rb65
-rw-r--r--spec/requests/api/todos_spec.rb15
-rw-r--r--spec/requests/api/triggers_spec.rb47
-rw-r--r--spec/requests/api/users_spec.rb229
-rw-r--r--spec/requests/api/variables_spec.rb38
-rw-r--r--spec/requests/ci/api/builds_spec.rb191
-rw-r--r--spec/requests/ci/api/triggers_spec.rb19
-rw-r--r--spec/requests/git_http_spec.rb57
-rw-r--r--spec/requests/lfs_http_spec.rb797
-rw-r--r--spec/routing/admin_routing_spec.rb5
-rw-r--r--spec/routing/project_routing_spec.rb17
-rw-r--r--spec/routing/routing_spec.rb30
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb104
-rw-r--r--spec/services/boards/create_service_spec.rb35
-rw-r--r--spec/services/boards/issues/list_service_spec.rb73
-rw-r--r--spec/services/boards/issues/move_service_spec.rb140
-rw-r--r--spec/services/boards/lists/create_service_spec.rb54
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb47
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb40
-rw-r--r--spec/services/boards/lists/move_service_spec.rb110
-rw-r--r--spec/services/ci/create_builds_service_spec.rb32
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb214
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb7
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb6
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb328
-rw-r--r--spec/services/ci/register_build_service_spec.rb2
-rw-r--r--spec/services/create_commit_builds_service_spec.rb240
-rw-r--r--spec/services/create_deployment_service_spec.rb16
-rw-r--r--spec/services/create_snippet_service_spec.rb4
-rw-r--r--spec/services/delete_user_service_spec.rb8
-rw-r--r--spec/services/destroy_group_service_spec.rb58
-rw-r--r--spec/services/event_create_service_spec.rb34
-rw-r--r--spec/services/files/update_service_spec.rb84
-rw-r--r--spec/services/git_hooks_service_spec.rb6
-rw-r--r--spec/services/git_push_service_spec.rb32
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb64
-rw-r--r--spec/services/issues/bulk_update_service_spec.rb125
-rw-r--r--spec/services/issues/close_service_spec.rb24
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/reopen_service_spec.rb25
-rw-r--r--spec/services/issues/update_service_spec.rb13
-rw-r--r--spec/services/merge_requests/build_service_spec.rb8
-rw-r--r--spec/services/merge_requests/close_service_spec.rb22
-rw-r--r--spec/services/merge_requests/create_service_spec.rb15
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb134
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb17
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb17
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb10
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb15
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb27
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service.rb46
-rw-r--r--spec/services/merge_requests/update_service_spec.rb13
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/milestones/create_service_spec.rb2
-rw-r--r--spec/services/notes/create_service_spec.rb34
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/slash_commands_service_spec.rb140
-rw-r--r--spec/services/notification_service_spec.rb158
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb14
-rw-r--r--spec/services/projects/create_service_spec.rb6
-rw-r--r--spec/services/projects/download_service_spec.rb4
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb27
-rw-r--r--spec/services/projects/fork_service_spec.rb8
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb44
-rw-r--r--spec/services/projects/update_service_spec.rb18
-rw-r--r--spec/services/projects/upload_service_spec.rb8
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb4
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb81
-rw-r--r--spec/services/search_service_spec.rb8
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb384
-rw-r--r--spec/services/system_note_service_spec.rb22
-rw-r--r--spec/services/test_hook_service_spec.rb4
-rw-r--r--spec/services/todo_service_spec.rb152
-rw-r--r--spec/simplecov_env.rb55
-rw-r--r--spec/spec_helper.rb15
-rw-r--r--spec/support/api/members_shared_examples.rb11
-rw-r--r--spec/support/api/schema_matcher.rb8
-rw-r--r--spec/support/api_helpers.rb14
-rw-r--r--spec/support/capybara_helpers.rb8
-rw-r--r--spec/support/email_helpers.rb10
-rw-r--r--spec/support/fake_u2f_device.rb9
-rw-r--r--spec/support/import_export/import_export.yml4
-rw-r--r--spec/support/issuable_create_service_slash_commands_shared_examples.rb83
-rw-r--r--spec/support/issuable_slash_commands_shared_examples.rb289
-rw-r--r--spec/support/issue_helpers.rb13
-rw-r--r--spec/support/login_helpers.rb34
-rw-r--r--spec/support/matchers/markdown_matchers.rb11
-rw-r--r--spec/support/merge_request_helpers.rb13
-rw-r--r--spec/support/omni_auth.rb1
-rw-r--r--spec/support/relative_url.rb8
-rw-r--r--spec/support/select2_helper.rb2
-rw-r--r--spec/support/test_env.rb39
-rw-r--r--spec/support/updating_mentions_shared_examples.rb32
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb16
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb8
-rw-r--r--spec/teaspoon_env.rb10
-rw-r--r--spec/uploaders/file_uploader_spec.rb45
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb20
-rw-r--r--spec/views/devise/shared/_signin_box.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/_head.html.haml_spec.rb36
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb39
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb21
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb26
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb37
-rw-r--r--spec/workers/build_email_worker_spec.rb2
-rw-r--r--spec/workers/email_receiver_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb66
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb27
-rw-r--r--spec/workers/group_destroy_worker_spec.rb19
-rw-r--r--spec/workers/post_receive_spec.rb14
-rw-r--r--spec/workers/project_destroy_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb58
-rw-r--r--spec/workers/repository_fork_worker_spec.rb10
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/Chart.js0
-rw-r--r--vendor/assets/javascripts/Sortable.js1285
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/autosize.js0
-rw-r--r--vendor/assets/javascripts/clipboard.js20
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/jquery.scrollTo.js0
-rw-r--r--vendor/assets/javascripts/task_list.js119
-rw-r--r--vendor/assets/javascripts/task_list.js.coffee258
-rw-r--r--vendor/assets/javascripts/vue-resource.full.js1318
-rw-r--r--vendor/assets/javascripts/vue-resource.js.erb2
-rw-r--r--vendor/assets/javascripts/vue-resource.min.js7
-rw-r--r--vendor/assets/javascripts/vue.full.js10073
-rw-r--r--vendor/assets/javascripts/vue.js.erb2
-rw-r--r--vendor/assets/javascripts/vue.min.js9
-rw-r--r--vendor/gitignore/Elm.gitignore2
-rw-r--r--vendor/gitignore/Global/VisualStudioCode.gitignore6
-rw-r--r--vendor/gitignore/Go.gitignore3
-rw-r--r--vendor/gitignore/Leiningen.gitignore3
-rw-r--r--vendor/gitignore/Objective-C.gitignore2
-rw-r--r--vendor/gitignore/Scala.gitignore4
-rw-r--r--vendor/gitignore/SugarCRM.gitignore2
-rw-r--r--vendor/gitignore/TeX.gitignore11
-rw-r--r--vendor/gitignore/Terraform.gitignore3
-rw-r--r--vendor/gitignore/Unity.gitignore3
-rw-r--r--vendor/gitlab-ci-yml/C++.gitlab-ci.yml26
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml2
-rw-r--r--vendor/gitlab-ci-yml/Grails.gitlab-ci.yml40
-rw-r--r--vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml11
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml26
-rw-r--r--vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml32
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml8
2113 files changed, 92898 insertions, 38477 deletions
diff --git a/.gitattributes b/.gitattributes
index 7e800609e6c..17cbaa5eef5 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,2 @@
-CHANGELOG merge=union \ No newline at end of file
+CHANGELOG merge=union
+*.js.es6 gitlab-language=javascript
diff --git a/.gitignore b/.gitignore
index ce6a363fe35..1bf9a47aef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@
/config/secrets.yml
/config/sidekiq.yml
/coverage/*
+/coverage-javascript/
/db/*.sqlite3
/db/*.sqlite3-journal
/db/data.yml
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ff8aa351226..be5614520a5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,11 +1,7 @@
-image: "ruby:2.1"
-
-services:
- - mysql:latest
- - redis:alpine
+image: "ruby:2.3.1"
cache:
- key: "ruby21"
+ key: "ruby-231"
paths:
- vendor/apt
- vendor/ruby
@@ -19,6 +15,7 @@ variables:
USE_DB: "true"
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
+ PHANTOMJS_VERSION: "2.1.1"
before_script:
- source ./scripts/prepare_build.sh
@@ -32,9 +29,9 @@ stages:
- prepare
- test
- post-test
+- pages
# Prepare and merge knapsack tests
-
.knapsack-state: &knapsack-state
services: []
variables:
@@ -45,6 +42,7 @@ stages:
paths:
- knapsack/
artifacts:
+ expire_in: 31d
paths:
- knapsack/
@@ -68,8 +66,14 @@ update-knapsack:
# Execute all testing suites
+.use-db: &use-db
+ services:
+ - mysql:latest
+ - redis:alpine
+
.rspec-knapsack: &rspec-knapsack
stage: test
+ <<: *use-db
script:
- bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
@@ -80,11 +84,14 @@ update-knapsack:
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- knapsack rspec
artifacts:
+ expire_in: 31d
paths:
- knapsack/
+ - coverage/
.spinach-knapsack: &spinach-knapsack
stage: test
+ <<: *use-db
script:
- bundle exec rake assets:precompile 2>/dev/null
- JOB_NAME=( $CI_BUILD_NAME )
@@ -95,8 +102,10 @@ update-knapsack:
- cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[ ! -e tmp/spinach-rerun.txt ] || bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
+ expire_in: 31d
paths:
- knapsack/
+ - coverage/
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
@@ -130,84 +139,138 @@ spinach 7 10: *spinach-knapsack
spinach 8 10: *spinach-knapsack
spinach 9 10: *spinach-knapsack
-# Execute all testing suites against Ruby 2.3
-.ruby-23: &ruby-23
- image: "ruby:2.3"
+# Execute all testing suites against Ruby 2.1
+.ruby-21: &ruby-21
+ image: "ruby:2.1"
+ <<: *use-db
only:
- master
cache:
- key: "ruby-23"
+ key: "ruby21"
paths:
- vendor/apt
- vendor/ruby
-.rspec-knapsack-ruby23: &rspec-knapsack-ruby23
+.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
<<: *rspec-knapsack
- <<: *ruby-23
+ <<: *ruby-21
-.spinach-knapsack-ruby23: &spinach-knapsack-ruby23
+.spinach-knapsack-ruby21: &spinach-knapsack-ruby21
<<: *spinach-knapsack
- <<: *ruby-23
-
-rspec 0 20 ruby23: *rspec-knapsack-ruby23
-rspec 1 20 ruby23: *rspec-knapsack-ruby23
-rspec 2 20 ruby23: *rspec-knapsack-ruby23
-rspec 3 20 ruby23: *rspec-knapsack-ruby23
-rspec 4 20 ruby23: *rspec-knapsack-ruby23
-rspec 5 20 ruby23: *rspec-knapsack-ruby23
-rspec 6 20 ruby23: *rspec-knapsack-ruby23
-rspec 7 20 ruby23: *rspec-knapsack-ruby23
-rspec 8 20 ruby23: *rspec-knapsack-ruby23
-rspec 9 20 ruby23: *rspec-knapsack-ruby23
-rspec 10 20 ruby23: *rspec-knapsack-ruby23
-rspec 11 20 ruby23: *rspec-knapsack-ruby23
-rspec 12 20 ruby23: *rspec-knapsack-ruby23
-rspec 13 20 ruby23: *rspec-knapsack-ruby23
-rspec 14 20 ruby23: *rspec-knapsack-ruby23
-rspec 15 20 ruby23: *rspec-knapsack-ruby23
-rspec 16 20 ruby23: *rspec-knapsack-ruby23
-rspec 17 20 ruby23: *rspec-knapsack-ruby23
-rspec 18 20 ruby23: *rspec-knapsack-ruby23
-rspec 19 20 ruby23: *rspec-knapsack-ruby23
-
-spinach 0 10 ruby23: *spinach-knapsack-ruby23
-spinach 1 10 ruby23: *spinach-knapsack-ruby23
-spinach 2 10 ruby23: *spinach-knapsack-ruby23
-spinach 3 10 ruby23: *spinach-knapsack-ruby23
-spinach 4 10 ruby23: *spinach-knapsack-ruby23
-spinach 5 10 ruby23: *spinach-knapsack-ruby23
-spinach 6 10 ruby23: *spinach-knapsack-ruby23
-spinach 7 10 ruby23: *spinach-knapsack-ruby23
-spinach 8 10 ruby23: *spinach-knapsack-ruby23
-spinach 9 10 ruby23: *spinach-knapsack-ruby23
+ <<: *ruby-21
+
+rspec 0 20 ruby21: *rspec-knapsack-ruby21
+rspec 1 20 ruby21: *rspec-knapsack-ruby21
+rspec 2 20 ruby21: *rspec-knapsack-ruby21
+rspec 3 20 ruby21: *rspec-knapsack-ruby21
+rspec 4 20 ruby21: *rspec-knapsack-ruby21
+rspec 5 20 ruby21: *rspec-knapsack-ruby21
+rspec 6 20 ruby21: *rspec-knapsack-ruby21
+rspec 7 20 ruby21: *rspec-knapsack-ruby21
+rspec 8 20 ruby21: *rspec-knapsack-ruby21
+rspec 9 20 ruby21: *rspec-knapsack-ruby21
+rspec 10 20 ruby21: *rspec-knapsack-ruby21
+rspec 11 20 ruby21: *rspec-knapsack-ruby21
+rspec 12 20 ruby21: *rspec-knapsack-ruby21
+rspec 13 20 ruby21: *rspec-knapsack-ruby21
+rspec 14 20 ruby21: *rspec-knapsack-ruby21
+rspec 15 20 ruby21: *rspec-knapsack-ruby21
+rspec 16 20 ruby21: *rspec-knapsack-ruby21
+rspec 17 20 ruby21: *rspec-knapsack-ruby21
+rspec 18 20 ruby21: *rspec-knapsack-ruby21
+rspec 19 20 ruby21: *rspec-knapsack-ruby21
+
+spinach 0 10 ruby21: *spinach-knapsack-ruby21
+spinach 1 10 ruby21: *spinach-knapsack-ruby21
+spinach 2 10 ruby21: *spinach-knapsack-ruby21
+spinach 3 10 ruby21: *spinach-knapsack-ruby21
+spinach 4 10 ruby21: *spinach-knapsack-ruby21
+spinach 5 10 ruby21: *spinach-knapsack-ruby21
+spinach 6 10 ruby21: *spinach-knapsack-ruby21
+spinach 7 10 ruby21: *spinach-knapsack-ruby21
+spinach 8 10 ruby21: *spinach-knapsack-ruby21
+spinach 9 10 ruby21: *spinach-knapsack-ruby21
# Other generic tests
+.ruby-static-analysis: &ruby-static-analysis
+ variables:
+ SIMPLECOV: "false"
+ USE_DB: "false"
+ USE_BUNDLE_INSTALL: "true"
+
.exec: &exec
+ <<: *ruby-static-analysis
stage: test
script:
- bundle exec $CI_BUILD_NAME
-teaspoon: *exec
rubocop: *exec
rake scss_lint: *exec
rake brakeman: *exec
rake flog: *exec
rake flay: *exec
-rake db:migrate:reset: *exec
license_finder: *exec
+rake downtime_check: *exec
+
+rake db:migrate:reset:
+ stage: test
+ <<: *use-db
+ script:
+ - rake db:migrate:reset
+
+teaspoon:
+ stage: test
+ <<: *use-db
+ script:
+ - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
+ - apt-get install --assume-yes nodejs
+ - npm install --global istanbul
+ - teaspoon
+ artifacts:
+ name: coverage-javascript
+ expire_in: 31d
+ paths:
+ - coverage-javascript/default/
+
+lint-doc:
+ stage: test
+ image: "phusion/baseimage:latest"
+ before_script: []
+ script:
+ - scripts/lint-doc.sh
bundler:audit:
stage: test
+ <<: *ruby-static-analysis
only:
- master
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+coverage:
+ stage: post-test
+ services: []
+ variables:
+ USE_DB: "false"
+ USE_BUNDLE_INSTALL: "true"
+ script:
+ - bundle exec scripts/merge-simplecov
+ artifacts:
+ name: coverage
+ expire_in: 31d
+ paths:
+ - coverage/index.html
+ - coverage/assets/
+
+
# Notify slack in the end
notify:slack:
stage: post-test
+ variables:
+ USE_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>"
when: on_failure
@@ -216,3 +279,20 @@ notify:slack:
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
+
+pages:
+ before_script: []
+ stage: pages
+ dependencies:
+ - coverage
+ - teaspoon
+ script:
+ - mv public/ .public/
+ - mkdir public/
+ - mv coverage public/coverage-ruby
+ - mv coverage-javascript/default/ public/coverage-javascript/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 00000000000..b676916fdf4
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,44 @@
+### Summary
+
+(Summarize the bug encountered concisely)
+
+### Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+### Expected behavior
+
+(What you should see instead)
+
+### Actual behaviour
+
+(What actually happens)
+
+### Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+### Output of checks
+
+#### Results of GitLab application Check
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
+
+#### Results of GitLab environment info
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:env:info`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+
+### Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
new file mode 100644
index 00000000000..ea895ee6275
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -0,0 +1,7 @@
+### Description
+
+(Include problem, use cases, benefits, and/or goals)
+
+### Proposal
+
+### Links / references
diff --git a/.hound.yml b/.hound.yml
deleted file mode 100644
index 3bde29fb2bf..00000000000
--- a/.hound.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-# Prefer single quotes
-StringLiterals:
- EnforcedStyle: single_quotes
- Enabled: true
diff --git a/.mailmap b/.mailmap
new file mode 100644
index 00000000000..bd5ac22132c
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,35 @@
+#
+# This list is used by git-shortlog to make contributions from the
+# same person appearing to be so.
+#
+
+Achilleas Pipinellis <axilleas@axilleas.me> <axilleas@archlinux.gr>
+Achilleas Pipinellis <axilleas@axilleas.me> <axilleas@users.noreply.github.com>
+Dmitriy Zaporozhets <dzaporozhets@gitlab.com> <dmitriy.zaporozhets@gmail.com>
+Dmitriy Zaporozhets <dzaporozhets@gitlab.com> <dzaporozhets@sphereconsultinginc.com>
+Douwe Maan <douwe@gitlab.com> <douwe@selenight.nl>
+Douwe Maan <douwe@gitlab.com> <me@douwe.me>
+Grzegorz Bizon <grzegorz@gitlab.com> <grzegorz.bizon@ntsn.pl>
+Grzegorz Bizon <grzegorz@gitlab.com> <grzesiek.bizon@gmail.com>
+Jacob Vosmaer <jacob@gitlab.com> <contact@jacobvosmaer.nl>
+Jacob Vosmaer <jacob@gitlab.com> Jacob Vosmaer (GitLab) <jacob@gitlab.com>
+Jacob Schatz <jschatz@gitlab.com> <jacobschatz@Jacobs-MacBook-Pro.local>
+Jacob Schatz <jschatz@gitlab.com> <jacobschatz@Jacobs-MBP.fios-router.home>
+Jacob Schatz <jschatz@gitlab.com> <jschatz1@gmail.com>
+James Lopez <james@jameslopez.es> <james@gitlab.com>
+James Lopez <james@jameslopez.es> <james.lopez@vodafone.com>
+Kamil Trzciński <kamil@gitlab.com> <ayufan@ayufan.eu>
+Marin Jankovski <maxlazio@gmail.com> <marin@gitlab.com>
+Phil Hughes <me@iamphill.com> <theephil@gmail.com>
+Rémy Coutable <remy@rymai.me> <remy@gitlab.com>
+Robert Schilling <rschilling@student.tugraz.at> <Razer6@users.noreply.github.com>
+Robert Schilling <rschilling@student.tugraz.at> <schilling.ro@gmail.com>
+Robert Speicher <robert@gitlab.com> <rspeicher@gmail.com>
+Stan Hu <stanhu@gmail.com> <stanhu@alum.mit.edu>
+Stan Hu <stanhu@gmail.com> <stanhu@packetzoom.com>
+Stan Hu <stanhu@gmail.com> <stanhu@users.noreply.github.com>
+Stan Hu <stanhu@gmail.com> stanhu <stanhu@gmail.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytse+admin@gitlab.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytse@dosire.com>
+Sytse Sijbrandij <sytse@gitlab.com> <sytses@gmail.com>
+Sytse Sijbrandij <sytse@gitlab.com> dosire <sytse@gitlab.com>
diff --git a/.rubocop.yml b/.rubocop.yml
index 3aac8401848..282f4539f03 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -2,6 +2,8 @@ require:
- rubocop-rspec
- ./rubocop/rubocop
+inherit_from: .rubocop_todo.yml
+
AllCops:
TargetRubyVersion: 2.1
# Cop names are not displayed in offense messages by default. Change behavior
@@ -52,14 +54,6 @@ Style/AlignArray:
Style/AlignHash:
Enabled: true
-# Align the parameters of a method call if they span more than one line.
-Style/AlignParameters:
- Enabled: false
-
-# Use &&/|| instead of and/or.
-Style/AndOr:
- Enabled: false
-
# Use `Array#join` instead of `Array#*`.
Style/ArrayJoin:
Enabled: true
@@ -80,10 +74,6 @@ Style/Attr:
Style/BeginBlock:
Enabled: true
-# Checks if usage of %() or %Q() matches configuration.
-Style/BarePercentLiterals:
- Enabled: false
-
# Do not use block comments.
Style/BlockComments:
Enabled: true
@@ -97,14 +87,6 @@ Style/BlockEndNewline:
Style/BlockDelimiters:
Enabled: true
-# Enforce braces style around hash parameters.
-Style/BracesAroundHashParameters:
- Enabled: false
-
-# Avoid explicit use of the case equality operator(===).
-Style/CaseEquality:
- Enabled: false
-
# Indentation of when in a case/when/[else/]end.
Style/CaseIndentation:
Enabled: true
@@ -133,24 +115,10 @@ Style/ClassMethods:
Style/ClassVars:
Enabled: true
-# Do not use :: for method call.
-Style/ColonMethodCall:
- Enabled: false
-
-# Checks formatting of special comments (TODO, FIXME, OPTIMIZE, HACK, REVIEW).
-Style/CommentAnnotation:
- Enabled: false
-
# Indentation of comments.
Style/CommentIndentation:
Enabled: true
-# Use the return value of `if` and `case` statements for assignment to a
-# variable and variable comparison instead of assigning that variable
-# inside of each branch.
-Style/ConditionalAssignment:
- Enabled: false
-
# Constants should use SCREAMING_SNAKE_CASE.
Style/ConstantName:
Enabled: true
@@ -159,34 +127,14 @@ Style/ConstantName:
Style/DefWithParentheses:
Enabled: true
-# Checks for use of deprecated Hash methods.
-Style/DeprecatedHashMethods:
- Enabled: false
-
# Document classes and non-namespace modules.
Style/Documentation:
Enabled: false
-# Checks the position of the dot in multi-line method calls.
-Style/DotPosition:
- Enabled: false
-
-# Checks for uses of double negation (!!).
-Style/DoubleNegation:
- Enabled: false
-
-# Prefer `each_with_object` over `inject` or `reduce`.
-Style/EachWithObject:
- Enabled: false
-
# Align elses and elsifs correctly.
Style/ElseAlignment:
Enabled: true
-# Avoid empty else-clauses.
-Style/EmptyElse:
- Enabled: false
-
# Use empty lines between defs.
Style/EmptyLineBetweenDefs:
Enabled: false
@@ -201,23 +149,19 @@ Style/EmptyLinesAroundAccessModifier:
# Keeps track of empty lines around block bodies.
Style/EmptyLinesAroundBlockBody:
- Enabled: false
+ Enabled: true
# Keeps track of empty lines around class bodies.
Style/EmptyLinesAroundClassBody:
- Enabled: false
+ Enabled: true
# Keeps track of empty lines around module bodies.
Style/EmptyLinesAroundModuleBody:
- Enabled: false
+ Enabled: true
# Keeps track of empty lines around method bodies.
Style/EmptyLinesAroundMethodBody:
- Enabled: false
-
-# Prefer literals to Array.new/Hash.new/String.new.
-Style/EmptyLiteral:
- Enabled: false
+ Enabled: true
# Avoid the use of END blocks.
Style/EndBlock:
@@ -231,10 +175,6 @@ Style/EndOfLine:
Style/EvenOdd:
Enabled: true
-# Do not use unnecessary spacing.
-Style/ExtraSpacing:
- Enabled: false
-
# Use snake_case for source file names.
Style/FileName:
Enabled: true
@@ -252,31 +192,15 @@ Style/FlipFlop:
Style/For:
Enabled: true
-# Enforce the use of Kernel#sprintf, Kernel#format or String#%.
-Style/FormatString:
- Enabled: false
-
# Do not introduce global variables.
Style/GlobalVars:
Enabled: true
-# Check for conditionals that can be replaced with guard clauses.
-Style/GuardClause:
- Enabled: false
-
# Prefer Ruby 1.9 hash syntax `{ a: 1, b: 2 }`
# over 1.8 syntax `{ :a => 1, :b => 2 }`.
Style/HashSyntax:
Enabled: true
-# Finds if nodes inside else, which can be converted to elsif.
-Style/IfInsideElse:
- Enabled: false
-
-# Favor modifier if/unless usage when you have a single-line body.
-Style/IfUnlessModifier:
- Enabled: false
-
# Do not use if x; .... Use the ternary operator instead.
Style/IfWithSemicolon:
Enabled: true
@@ -299,22 +223,10 @@ Style/IndentationConsistency:
Style/IndentationWidth:
Enabled: true
-# Checks the indentation of the first element in an array literal.
-Style/IndentArray:
- Enabled: false
-
-# Checks the indentation of the first key in a hash literal.
-Style/IndentHash:
- Enabled: false
-
# Use Kernel#loop for infinite loops.
Style/InfiniteLoop:
Enabled: true
-# Use the new lambda literal syntax for single-line blocks.
-Style/Lambda:
- Enabled: false
-
# Use lambda.call(...) instead of lambda.(...).
Style/LambdaCall:
Enabled: true
@@ -323,14 +235,6 @@ Style/LambdaCall:
Style/LeadingCommentSpace:
Enabled: true
-# Use \ instead of + or << to concatenate two string literals at line end.
-Style/LineEndConcatenation:
- Enabled: false
-
-# Do not use parentheses for method calls with no arguments.
-Style/MethodCallParentheses:
- Enabled: false
-
# Checks if the method definitions have or don't have parentheses.
Style/MethodDefParentheses:
Enabled: true
@@ -389,37 +293,20 @@ Style/MultilineOperationIndentation:
# Avoid multi-line `? :` (the ternary operator), use if/unless instead.
Style/MultilineTernaryOperator:
- Enabled: false
-
-# Do not assign mutable objects to constants.
-Style/MutableConstant:
- Enabled: false
+ Enabled: true
# Favor unless over if for negative conditions (or control flow or).
Style/NegatedIf:
Enabled: true
-# Favor until over while for negative conditions.
-Style/NegatedWhile:
- Enabled: false
-
# Avoid using nested modifiers.
Style/NestedModifier:
Enabled: true
-# Parenthesize method calls which are nested inside the argument list of
-# another parenthesized method call.
-Style/NestedParenthesizedCalls:
- Enabled: false
-
# Use one expression per branch in a ternary operator.
Style/NestedTernaryOperator:
Enabled: true
-# Use `next` to skip iteration instead of a condition at the end.
-Style/Next:
- Enabled: false
-
# Prefer x.nil? to x == nil.
Style/NilComparison:
Enabled: true
@@ -444,51 +331,10 @@ Style/OneLineConditional:
Style/OpMethod:
Enabled: true
-# Check for simple usages of parallel assignment. It will only warn when
-# the number of variables matches on both sides of the assignment.
-Style/ParallelAssignment:
- Enabled: false
-
# Don't use parentheses around the condition of an if/unless/while.
Style/ParenthesesAroundCondition:
Enabled: true
-# Use `%`-literal delimiters consistently.
-Style/PercentLiteralDelimiters:
- Enabled: false
-
-# Checks if uses of %Q/%q match the configured preference.
-Style/PercentQLiterals:
- Enabled: false
-
-# Avoid Perl-style regex back references.
-Style/PerlBackrefs:
- Enabled: false
-
-# Check the names of predicate methods.
-Style/PredicateName:
- Enabled: false
-
-# Use proc instead of Proc.new.
-Style/Proc:
- Enabled: false
-
-# Checks the arguments passed to raise/fail.
-Style/RaiseArgs:
- Enabled: false
-
-# Don't use begin blocks when they are not needed.
-Style/RedundantBegin:
- Enabled: false
-
-# Checks for an obsolete RuntimeException argument in raise/fail.
-Style/RedundantException:
- Enabled: false
-
-# Checks usages of Object#freeze on immutable objects.
-Style/RedundantFreeze:
- Enabled: false
-
# Checks for parentheses that seem not to serve any purpose.
Style/RedundantParentheses:
Enabled: true
@@ -497,24 +343,6 @@ Style/RedundantParentheses:
Style/RedundantReturn:
Enabled: true
-# Don't use self where it's not needed.
-Style/RedundantSelf:
- Enabled: false
-
-# Use %r for regular expressions matching more than `MaxSlashes` '/'
-# characters. Use %r only for regular expressions matching more
-# than `MaxSlashes` '/' character.
-Style/RegexpLiteral:
- Enabled: false
-
-# Avoid using rescue in its modifier form.
-Style/RescueModifier:
- Enabled: false
-
-# Checks for places where self-assignment shorthand should have been used.
-Style/SelfAssignment:
- Enabled: false
-
# Don't use semicolons to terminate expressions.
Style/Semicolon:
Enabled: true
@@ -524,14 +352,6 @@ Style/SignalException:
EnforcedStyle: only_raise
Enabled: true
-# Enforces the names of some block params.
-Style/SingleLineBlockParams:
- Enabled: false
-
-# Avoid single-line methods.
-Style/SingleLineMethods:
- Enabled: false
-
# Use spaces after colons.
Style/SpaceAfterColon:
Enabled: true
@@ -553,10 +373,9 @@ Style/SpaceAfterNot:
Style/SpaceAfterSemicolon:
Enabled: true
-# Checks that the equals signs in parameter default assignments have or don't
-# have surrounding space depending on configuration.
+# Use space around equals in parameter default
Style/SpaceAroundEqualsInParameterDefault:
- Enabled: false
+ Enabled: true
# Use a space around keywords if appropriate.
Style/SpaceAroundKeyword:
@@ -566,10 +385,6 @@ Style/SpaceAroundKeyword:
Style/SpaceAroundOperators:
Enabled: true
-# Checks that the left block brace has or doesn't have space before it.
-Style/SpaceBeforeBlockBraces:
- Enabled: false
-
# No spaces before commas.
Style/SpaceBeforeComma:
Enabled: true
@@ -578,33 +393,14 @@ Style/SpaceBeforeComma:
Style/SpaceBeforeComment:
Enabled: true
-# Checks that exactly one space is used between a method name and the first
-# argument for method calls without parentheses.
-Style/SpaceBeforeFirstArg:
- Enabled: false
-
# No spaces before semicolons.
Style/SpaceBeforeSemicolon:
Enabled: true
-# Checks that block braces have or don't have surrounding space.
-# For blocks taking parameters, checks that the left brace has or doesn't
-# have trailing space.
-Style/SpaceInsideBlockBraces:
- Enabled: false
-
-# No spaces after [ or before ].
-Style/SpaceInsideBrackets:
- Enabled: false
-
# Use spaces inside hash literal braces - or don't.
Style/SpaceInsideHashLiteralBraces:
Enabled: true
-# No spaces after ( or before ).
-Style/SpaceInsideParens:
- Enabled: false
-
# No spaces inside range literals.
Style/SpaceInsideRangeLiteral:
Enabled: true
@@ -614,10 +410,6 @@ Style/SpaceInsideStringInterpolation:
EnforcedStyle: no_space
Enabled: true
-# Avoid Perl-style global variables.
-Style/SpecialGlobalVars:
- Enabled: false
-
# Check for the usage of parentheses around stabby lambda arguments.
Style/StabbyLambdaParentheses:
EnforcedStyle: require_parentheses
@@ -627,25 +419,12 @@ Style/StabbyLambdaParentheses:
Style/StringLiterals:
Enabled: false
-# Checks if uses of quotes inside expressions in interpolated strings match the
-# configured preference.
-Style/StringLiteralsInInterpolation:
- Enabled: false
-
# Checks if configured preferred methods are used over non-preferred.
Style/StringMethods:
PreferredMethods:
intern: to_sym
Enabled: true
-# Use %i or %I for arrays of symbols.
-Style/SymbolArray:
- Enabled: false
-
-# Use symbols as procs instead of blocks when possible.
-Style/SymbolProc:
- Enabled: false
-
# No hard tabs.
Style/Tab:
Enabled: true
@@ -654,40 +433,10 @@ Style/Tab:
Style/TrailingBlankLines:
Enabled: true
-# Checks for trailing comma in array and hash literals.
-Style/TrailingCommaInLiteral:
- Enabled: false
-
-# Checks for trailing comma in argument lists.
-Style/TrailingCommaInArguments:
- Enabled: false
-
-# Avoid trailing whitespace.
-Style/TrailingWhitespace:
- Enabled: false
-
-# Checks for the usage of unneeded trailing underscores at the end of
-# parallel variable assignment.
-Style/TrailingUnderscoreVariable:
- Enabled: false
-
-# Prefer attr_* methods to trivial readers/writers.
-Style/TrivialAccessors:
- Enabled: false
-
-# Do not use unless with else. Rewrite these with the positive case first.
-Style/UnlessElse:
- Enabled: false
-
# Checks for %W when interpolation is not needed.
Style/UnneededCapitalW:
Enabled: true
-# TODO: Enable UnneededInterpolation Cop.
-# Checks for strings that are just an interpolated expression.
-Style/UnneededInterpolation:
- Enabled: false
-
# Checks for %q/%Q when single quotes or double quotes would do.
Style/UnneededPercentQ:
Enabled: false
@@ -717,12 +466,6 @@ Style/WhileUntilModifier:
Style/WordArray:
Enabled: false
-# TODO: Enable ZeroLengthPredicate Cop.
-# Use #empty? when testing for objects of length 0.
-Style/ZeroLengthPredicate:
- Enabled: false
-
-
#################### Metrics ################################
# A calculated magnitude based on number of assignments,
@@ -771,19 +514,19 @@ Metrics/PerceivedComplexity:
#################### Lint ################################
-# Checks for ambiguous operators in the first argument of a method invocation
-# without parentheses.
-Lint/AmbiguousOperator:
+# Checks for useless access modifiers.
+Lint/UselessAccessModifier:
Enabled: true
-# Checks for ambiguous regexp literals in the first argument of a method
-# invocation without parentheses.
-Lint/AmbiguousRegexpLiteral:
+# Checks for attempts to use `private` or `protected` to set the visibility
+# of a class method, which does not work.
+Lint/IneffectiveAccessModifier:
Enabled: false
-# Don't use assignment in conditions.
-Lint/AssignmentInCondition:
- Enabled: false
+# Checks for ambiguous operators in the first argument of a method invocation
+# without parentheses.
+Lint/AmbiguousOperator:
+ Enabled: true
# Align block ends correctly.
Lint/BlockAlignment:
@@ -810,14 +553,6 @@ Lint/DefEndAlignment:
Lint/DeprecatedClassMethods:
Enabled: true
-# Check for duplicate method definitions.
-Lint/DuplicateMethods:
- Enabled: false
-
-# Check for duplicate keys in hash literals.
-Lint/DuplicatedKey:
- Enabled: false
-
# Check for immutable argument given to each_with_object.
Lint/EachWithObjectArgument:
Enabled: true
@@ -830,10 +565,6 @@ Lint/ElseLayout:
Lint/EmptyEnsure:
Enabled: true
-# Checks for empty string interpolation.
-Lint/EmptyInterpolation:
- Enabled: false
-
# Align ends correctly.
Lint/EndAlignment:
Enabled: true
@@ -858,21 +589,11 @@ Lint/FloatOutOfRange:
Lint/FormatParameterMismatch:
Enabled: true
-# Don't suppress exception.
-Lint/HandleExceptions:
- Enabled: false
-
# Checks for adjacent string literals on the same line, which could better be
# represented as a single string literal.
Lint/ImplicitStringConcatenation:
Enabled: true
-# TODO: Enable IneffectiveAccessModifier Cop.
-# Checks for attempts to use `private` or `protected` to set the visibility
-# of a class method, which does not work.
-Lint/IneffectiveAccessModifier:
- Enabled: false
-
# Checks for invalid character literals with a non-escaped whitespace
# character.
Lint/InvalidCharacterLiteral:
@@ -886,11 +607,6 @@ Lint/LiteralInCondition:
Lint/LiteralInInterpolation:
Enabled: true
-# Use Kernel#loop with break rather than begin/end/until or begin/end/while
-# for post-loop tests.
-Lint/Loop:
- Enabled: false
-
# Do not use nested method definitions.
Lint/NestedMethodDefinition:
Enabled: true
@@ -916,13 +632,8 @@ Lint/RequireParentheses:
Lint/RescueException:
Enabled: true
-# Do not use the same name as outer local variable for block arguments
-# or block local variables.
-Lint/ShadowingOuterLocalVariable:
- Enabled: false
-
-# 'Checks for Object#to_s usage in string interpolation.
-Lint/StringConversionInInterpolation:
+# Checks for the order which exceptions are rescued to avoid rescueing a less specific exception before a more specific exception.
+Lint/ShadowedException:
Enabled: false
# Do not use prefix `_` for a variable that is used.
@@ -935,22 +646,10 @@ Lint/UnderscorePrefixedVariableName:
Lint/UnneededDisable:
Enabled: false
-# Checks for unused block arguments.
-Lint/UnusedBlockArgument:
- Enabled: false
-
-# Checks for unused method arguments.
-Lint/UnusedMethodArgument:
- Enabled: false
-
# Unreachable code.
Lint/UnreachableCode:
Enabled: true
-# Checks for useless access modifiers.
-Lint/UselessAccessModifier:
- Enabled: false
-
# Checks for useless assignment to a local variable.
Lint/UselessAssignment:
Enabled: true
@@ -983,11 +682,6 @@ Performance/Casecmp:
Performance/DoubleStartEndWith:
Enabled: true
-# TODO: Enable EndWith Cop.
-# Use `end_with?` instead of a regex match anchored to the end of a string.
-Performance/EndWith:
- Enabled: false
-
# Use `strip` instead of `lstrip.rstrip`.
Performance/LstripRstrip:
Enabled: true
@@ -996,24 +690,6 @@ Performance/LstripRstrip:
Performance/RangeInclude:
Enabled: true
-# TODO: Enable RedundantBlockCall Cop.
-# Use `yield` instead of `block.call`.
-Performance/RedundantBlockCall:
- Enabled: false
-
-# TODO: Enable RedundantMatch Cop.
-# Use `=~` instead of `String#match` or `Regexp#match` in a context where the
-# returned `MatchData` is not needed.
-Performance/RedundantMatch:
- Enabled: false
-
-# TODO: Enable RedundantMerge Cop.
-# Use `Hash#[]=`, rather than `Hash#merge!` with a single key-value pair.
-Performance/RedundantMerge:
- # Max number of key-value pairs to consider an offense
- MaxKeyValuePairs: 2
- Enabled: false
-
# Use `sort` instead of `sort_by { |x| x }`.
Performance/RedundantSortBy:
Enabled: true
@@ -1082,18 +758,6 @@ Rails/ReadWriteAttribute:
Rails/ScopeArgs:
Enabled: true
-# Checks the correct usage of time zone aware methods.
-# http://danilenko.org/2012/7/6/rails_timezones
-Rails/TimeZone:
- Enabled: false
-
-# Use validates :attribute, hash of validations.
-Rails/Validation:
- Enabled: false
-
-Rails/UniqBeforePluck:
- Enabled: false
-
##################### RSpec ##################################
# Check that instances are not being stubbed globally.
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 00000000000..20daf1619a7
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,443 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config --exclude-limit 0`
+# on 2016-07-13 12:36:08 -0600 using RuboCop version 0.41.2.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 154
+Lint/AmbiguousRegexpLiteral:
+ Enabled: false
+
+# Offense count: 43
+# Configuration parameters: AllowSafeAssignment.
+Lint/AssignmentInCondition:
+ Enabled: false
+
+# Offense count: 14
+Lint/HandleExceptions:
+ Enabled: false
+
+# Offense count: 2
+Lint/Loop:
+ Enabled: false
+
+# Offense count: 15
+Lint/ShadowingOuterLocalVariable:
+ Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Lint/StringConversionInInterpolation:
+ Enabled: false
+
+# Offense count: 44
+# Cop supports --auto-correct.
+# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+# Offense count: 129
+# Cop supports --auto-correct.
+# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
+Lint/UnusedMethodArgument:
+ Enabled: false
+
+# Offense count: 12
+# Cop supports --auto-correct.
+Performance/PushSplat:
+ Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Performance/RedundantBlockCall:
+ Enabled: false
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Performance/RedundantMatch:
+ Enabled: false
+
+# Offense count: 24
+# Cop supports --auto-correct.
+# Configuration parameters: MaxKeyValuePairs.
+Performance/RedundantMerge:
+ Enabled: false
+
+# Offense count: 60
+Rails/OutputSafety:
+ Enabled: false
+
+# Offense count: 128
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: strict, flexible
+Rails/TimeZone:
+ Enabled: false
+
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/Validation:
+ Enabled: false
+
+# Offense count: 217
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
+# SupportedStyles: with_first_parameter, with_fixed_indentation
+Style/AlignParameters:
+ Enabled: false
+
+# Offense count: 32
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: always, conditionals
+Style/AndOr:
+ Enabled: false
+
+# Offense count: 47
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: percent_q, bare_percent
+Style/BarePercentLiterals:
+ Enabled: false
+
+# Offense count: 258
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: braces, no_braces, context_dependent
+Style/BracesAroundHashParameters:
+ Enabled: false
+
+# Offense count: 5
+Style/CaseEquality:
+ Enabled: false
+
+# Offense count: 19
+# Cop supports --auto-correct.
+Style/ColonMethodCall:
+ Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: Keywords.
+# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
+Style/CommentAnnotation:
+ Enabled: false
+
+# Offense count: 34
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
+# SupportedStyles: assign_to_condition, assign_inside_condition
+Style/ConditionalAssignment:
+ Enabled: false
+
+# Offense count: 789
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: leading, trailing
+Style/DotPosition:
+ Enabled: false
+
+# Offense count: 13
+Style/DoubleNegation:
+ Enabled: false
+
+# Offense count: 3
+Style/EachWithObject:
+ Enabled: false
+
+# Offense count: 30
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: empty, nil, both
+Style/EmptyElse:
+ Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/EmptyLiteral:
+ Enabled: false
+
+# Offense count: 123
+# Cop supports --auto-correct.
+# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
+Style/ExtraSpacing:
+ Enabled: false
+
+# Offense count: 7
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: format, sprintf, percent
+Style/FormatString:
+ Enabled: false
+
+# Offense count: 48
+# Configuration parameters: MinBodyLength.
+Style/GuardClause:
+ Enabled: false
+
+# Offense count: 11
+Style/IfInsideElse:
+ Enabled: false
+
+# Offense count: 177
+# Cop supports --auto-correct.
+# Configuration parameters: MaxLineLength.
+Style/IfUnlessModifier:
+ Enabled: false
+
+# Offense count: 52
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
+# SupportedStyles: special_inside_parentheses, consistent, align_brackets
+Style/IndentArray:
+ Enabled: false
+
+# Offense count: 89
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
+# SupportedStyles: special_inside_parentheses, consistent, align_braces
+Style/IndentHash:
+ Enabled: false
+
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: line_count_dependent, lambda, literal
+Style/Lambda:
+ Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/LineEndConcatenation:
+ Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Style/MethodCallParentheses:
+ Enabled: false
+
+# Offense count: 62
+# Cop supports --auto-correct.
+Style/MutableConstant:
+ Enabled: false
+
+# Offense count: 10
+# Cop supports --auto-correct.
+Style/NestedParenthesizedCalls:
+ Enabled: false
+
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
+# SupportedStyles: skip_modifier_ifs, always
+Style/Next:
+ Enabled: false
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
+# SupportedOctalStyles: zero_with_o, zero_only
+Style/NumericLiteralPrefix:
+ Enabled: false
+
+# Offense count: 29
+# Cop supports --auto-correct.
+Style/ParallelAssignment:
+ Enabled: false
+
+# Offense count: 208
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredDelimiters.
+Style/PercentLiteralDelimiters:
+ Enabled: false
+
+# Offense count: 11
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: lower_case_q, upper_case_q
+Style/PercentQLiterals:
+ Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Style/PerlBackrefs:
+ Enabled: false
+
+# Offense count: 32
+# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
+# NamePrefix: is_, has_, have_
+# NamePrefixBlacklist: is_, has_, have_
+# NameWhitelist: is_a?
+Style/PredicateName:
+ Enabled: false
+
+# Offense count: 28
+# Cop supports --auto-correct.
+Style/PreferredHashMethods:
+ Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/Proc:
+ Enabled: false
+
+# Offense count: 20
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: compact, exploded
+Style/RaiseArgs:
+ Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/RedundantBegin:
+ Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantException:
+ Enabled: false
+
+# Offense count: 23
+# Cop supports --auto-correct.
+Style/RedundantFreeze:
+ Enabled: false
+
+# Offense count: 377
+# Cop supports --auto-correct.
+Style/RedundantSelf:
+ Enabled: false
+
+# Offense count: 94
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
+# SupportedStyles: slashes, percent_r, mixed
+Style/RegexpLiteral:
+ Enabled: false
+
+# Offense count: 17
+# Cop supports --auto-correct.
+Style/RescueModifier:
+ Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/SelfAssignment:
+ Enabled: false
+
+# Offense count: 2
+# Configuration parameters: Methods.
+# Methods: {"reduce"=>["a", "e"]}, {"inject"=>["a", "e"]}
+Style/SingleLineBlockParams:
+ Enabled: false
+
+# Offense count: 50
+# Cop supports --auto-correct.
+# Configuration parameters: AllowIfMethodIsEmpty.
+Style/SingleLineMethods:
+ Enabled: false
+
+# Offense count: 119
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: space, no_space
+Style/SpaceBeforeBlockBraces:
+ Enabled: false
+
+# Offense count: 11
+# Cop supports --auto-correct.
+# Configuration parameters: AllowForAlignment.
+Style/SpaceBeforeFirstArg:
+ Enabled: false
+
+# Offense count: 130
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+# SupportedStyles: space, no_space
+Style/SpaceInsideBlockBraces:
+ Enabled: false
+
+# Offense count: 98
+# Cop supports --auto-correct.
+Style/SpaceInsideBrackets:
+ Enabled: false
+
+# Offense count: 60
+# Cop supports --auto-correct.
+Style/SpaceInsideParens:
+ Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/SpaceInsidePercentLiteralDelimiters:
+ Enabled: false
+
+# Offense count: 36
+# Cop supports --auto-correct.
+# Configuration parameters: SupportedStyles.
+# SupportedStyles: use_perl_names, use_english_names
+Style/SpecialGlobalVars:
+ EnforcedStyle: use_perl_names
+
+# Offense count: 30
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: single_quotes, double_quotes
+Style/StringLiteralsInInterpolation:
+ Enabled: false
+
+# Offense count: 24
+# Cop supports --auto-correct.
+# Configuration parameters: IgnoredMethods.
+# IgnoredMethods: respond_to, define_method
+Style/SymbolProc:
+ Enabled: false
+
+# Offense count: 23
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
+# SupportedStyles: comma, consistent_comma, no_comma
+Style/TrailingCommaInArguments:
+ Enabled: false
+
+# Offense count: 113
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
+# SupportedStyles: comma, consistent_comma, no_comma
+Style/TrailingCommaInLiteral:
+ Enabled: false
+
+# Offense count: 7
+# Cop supports --auto-correct.
+# Configuration parameters: AllowNamedUnderscoreVariables.
+Style/TrailingUnderscoreVariable:
+ Enabled: false
+
+# Offense count: 90
+# Cop supports --auto-correct.
+Style/TrailingWhitespace:
+ Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist.
+# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
+Style/TrivialAccessors:
+ Enabled: false
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/UnlessElse:
+ Enabled: false
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Style/UnneededInterpolation:
+ Enabled: false
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Style/ZeroLengthPredicate:
+ Enabled: false
diff --git a/.ruby-version b/.ruby-version
index ebf14b46981..2bf1c1ccf36 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.8
+2.3.1
diff --git a/.simplecov b/.simplecov
deleted file mode 100644
index d979288df44..00000000000
--- a/.simplecov
+++ /dev/null
@@ -1,4 +0,0 @@
-# .simplecov
-SimpleCov.start 'rails' do
- merge_timeout 3600
-end
diff --git a/.teatro.yml b/.teatro.yml
deleted file mode 100644
index 30054361981..00000000000
--- a/.teatro.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-stage:
- before:
- - cp config/gitlab.teatro.yml config/gitlab.yml
- - mkdir /apps/gitlab-satellites
- - mkdir /apps/repositories
-
- database:
- - RAILS_ENV=development force=yes bundle exec rake db:create gitlab:setup \ No newline at end of file
diff --git a/CHANGELOG b/CHANGELOG
index a977fc3fdbf..a6cd8f4c7e1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,68 +1,465 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.10.0 (unreleased)
+v 8.12.0 (unreleased)
+ - Make push events have equal vertical spacing.
+ - Add two-factor recovery endpoint to internal API !5510
+ - Add font color contrast to external label in admin area (ClemMakesApps)
+ - Change logo animation to CSS (ClemMakesApps)
+ - Change merge_error column from string to text type
+ - Reduce contributions calendar data payload (ClemMakesApps)
+ - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
+ - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+ - Add hover color to emoji icon (ClemMakesApps)
+ - Fix branches page dropdown sort alignment (ClemMakesApps)
+ - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+ - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
+ - Remove Gitorious import
+ - Add Sentry logging to API calls
+ - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Remove unused mixins (ClemMakesApps)
+ - Fix groups sort dropdown alignment (ClemMakesApps)
+ - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
+ - Fix markdown help references (ClemMakesApps)
+ - Added tests for diff notes
+ - Add delimiter to project stars and forks count (ClemMakesApps)
+ - Fix badge count alignment (ClemMakesApps)
+ - Fix branch title trailing space on hover (ClemMakesApps)
+ - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+ - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
+ - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
+ - Update merge_requests.md with a simpler way to check out a merge request. !5944
+ - Fix button missing type (ClemMakesApps)
+ - Move to project dropdown with infinite scroll for better performance
+ - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz)
+ - Load branches asynchronously in Cherry Pick and Revert dialogs.
+ - Add merge request versions !5467
+ - Change using size to use count and caching it for number of group members. !5935
+ - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
+ - Reduce number of database queries on builds tab
+ - Capitalize mentioned issue timeline notes (ClemMakesApps)
+ - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
+ - Adds response mime type to transaction metric action when it's not HTML
+ - Fix hover leading space bug in pipeline graph !5980
+
+v 8.11.3 (unreleased)
+ - Allow system info page to handle case where info is unavailable
+ - Label list shows all issues (opened or closed) with that label
+ - Don't show resolve conflicts link before MR status is updated
+ - Fix IE11 fork button bug !598
+ - Don't prevent viewing the MR when git refs for conflicts can't be found on disk
+ - Fix external issue tracker "Issues" link leading to 404s
+
+v 8.11.2
+ - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
+ - Use gitlab-workhorse 0.7.11 !5983
+ - Does not halt the GitHub import process when an error occurs. !5763
+ - Fix file links on project page when default view is Files !5933
+ - Fixed enter key in search input not working !5888
+
+v 8.11.1
+ - Pulled due to packaging error.
+
+v 8.11.0
+ - Use test coverage value from the latest successful pipeline in badge. !5862
+ - Add test coverage report badge. !5708
+ - Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
+ - Add Koding (online IDE) integration
+ - Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
+ - Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
+ - Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
+ - Fix adding line comments on the initial commit to a repo !5900
+ - Fix the title of the toggle dropdown button. !5515 (herminiotorres)
+ - Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
+ - Update to Ruby 2.3.1. !4948
+ - Add Issues Board !5548
+ - Allow resolving merge conflicts in the UI !5479
+ - Improve diff performance by eliminating redundant checks for text blobs
+ - Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
+ - Convert switch icon into icon font (ClemMakesApps)
+ - API: Endpoints for enabling and disabling deploy keys
+ - API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
+ - Use long options for curl examples in documentation !5703 (winniehell)
+ - Added tooltip listing label names to the labels value in the collapsed issuable sidebar
+ - Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
+ - GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
+ - Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
+ - Allow naming U2F devices !5833
+ - Ignore URLs starting with // in Markdown links !5677 (winniehell)
+ - Fix CI status icon link underline (ClemMakesApps)
+ - The Repository class is now instrumented
+ - Fix commit mention font inconsistency (ClemMakesApps)
+ - Do not escape URI when extracting path !5878 (winniehell)
+ - Fix filter label tooltip HTML rendering (ClemMakesApps)
+ - Cache the commit author in RequestStore to avoid extra lookups in PostReceive
+ - Expand commit message width in repo view (ClemMakesApps)
+ - Cache highlighted diff lines for merge requests
+ - Pre-create all builds for a Pipeline when the new Pipeline is created !5295
+ - Allow merge request diff notes and discussions to be explicitly marked as resolved
+ - API: Add deployment endpoints
+ - API: Add Play endpoint on Builds
+ - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+ - Show wall clock time when showing a pipeline. !5734
+ - Show member roles to all users on members page
+ - Project.visible_to_user is instrumented again
+ - Fix awardable button mutuality loading spinners (ClemMakesApps)
+ - Sort todos by date and priority
+ - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
+ - Optimize maximum user access level lookup in loading of notes
+ - Send notification emails to users newly mentioned in issue and MR edits !5800
+ - Add "No one can push" as an option for protected branches. !5081
+ - Improve performance of AutolinkFilter#text_parse by using XPath
+ - Add experimental Redis Sentinel support !1877
+ - Rendering of SVGs as blobs is now limited to SVGs with a size smaller or equal to 2MB
+ - Fix branches page dropdown sort initial state (ClemMakesApps)
+ - Environments have an url to link to
+ - Various redundant database indexes have been removed
+ - Update `timeago` plugin to use multiple string/locale settings
+ - Remove unused images (ClemMakesApps)
+ - Get issue and merge request description templates from repositories
+ - Add hover state to todos !5361 (winniehell)
+ - Fix icon alignment of star and fork buttons !5451 (winniehell)
+ - Enforce 2FA restrictions on API authentication endpoints !5820
+ - Limit git rev-list output count to one in forced push check
+ - Show deployment status on merge requests with external URLs
+ - Clean up unused routes (Josef Strzibny)
+ - Fix issue on empty project to allow developers to only push to protected branches if given permission
+ - API: Add enpoints for pipelines
+ - Add green outline to New Branch button. !5447 (winniehell)
+ - Optimize generating of cache keys for issues and notes
+ - Fix repository push email formatting in Outlook
+ - Improve performance of syntax highlighting Markdown code blocks
+ - Update to gitlab_git 10.4.1 and take advantage of preserved Ref objects
+ - Remove delay when hitting "Reply..." button on page with a lot of discussions
+ - Retrieve rendered HTML from cache in one request
+ - Fix renaming repository when name contains invalid chararacters under project settings
+ - Upgrade Grape from 0.13.0 to 0.15.0. !4601
+ - Trigram indexes for the "ci_runners" table have been removed to speed up UPDATE queries
+ - Fix devise deprecation warnings.
+ - Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
+ - Update version_sorter and use new interface for faster tag sorting
+ - Optimize checking if a user has read access to a list of issues !5370
+ - Store all DB secrets in secrets.yml, under descriptive names !5274
+ - Fix syntax highlighting in file editor
+ - Support slash commands in issue and merge request descriptions as well as comments. !5021
+ - Nokogiri's various parsing methods are now instrumented
+ - Add archived badge to project list !5798
+ - Add simple identifier to public SSH keys (muteor)
+ - Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
+ - Fix filter input alignment (ClemMakesApps)
+ - Include old revision in merge request update hooks (Ben Boeckel)
+ - Add build event color in HipChat messages (David Eisner)
+ - Make fork counter always clickable. !5463 (winniehell)
+ - Document that webhook secret token is sent in X-Gitlab-Token HTTP header !5664 (lycoperdon)
+ - Gitlab::Highlight is now instrumented
+ - All created issues, API or WebUI, can be submitted to Akismet for spam check !5333
+ - Allow users to import cross-repository pull requests from GitHub
+ - The overhead of instrumented method calls has been reduced
+ - Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
+ - Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
+ - Add pipeline events hook
+ - Bump gitlab_git to speedup DiffCollection iterations
+ - Rewrite description of a blocked user in admin settings. (Elias Werberich)
+ - Make branches sortable without push permission !5462 (winniehell)
+ - Check for Ci::Build artifacts at database level on pipeline partial
+ - Convert image diff background image to CSS (ClemMakesApps)
+ - Remove unnecessary index_projects_on_builds_enabled index from the projects table
+ - Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
+ - Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
+ - Fix search for notes which belongs to deleted objects
+ - Allow Akismet to be trained by submitting issues as spam or ham !5538
+ - Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
+ - Allow branch names ending with .json for graph and network page !5579 (winniehell)
+ - Add the `sprockets-es6` gem
+ - Improve OAuth2 client documentation (muteor)
+ - Fix diff comments inverted toggle bug (ClemMakesApps)
+ - Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
+ - Profile requests when a header is passed
+ - Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
+ - Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
+ - Add commit stats in commit api. !5517 (dixpac)
+ - Add CI configuration button on project page
+ - Fix merge request new view not changing code view rendering style
+ - edit_blob_link will use blob passed onto the options parameter
+ - Make error pages responsive (Takuya Noguchi)
+ - The performance of the project dropdown used for moving issues has been improved
+ - Fix skip_repo parameter being ignored when destroying a namespace
+ - Add all builds into stage/job dropdowns on builds page
+ - Change requests_profiles resource constraint to catch virtually any file
+ - Bump gitlab_git to lazy load compare commits
+ - Reduce number of queries made for merge_requests/:id/diffs
+ - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
+ - Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
+ - Fix bug where destroying a namespace would not always destroy projects
+ - Fix RequestProfiler::Middleware error when code is reloaded in development
+ - Allow horizontal scrolling of code blocks in issue body
+ - Catch what warden might throw when profiling requests to re-throw it
+ - Avoid commit lookup on diff_helper passing existing local variable to the helper method
+ - Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac)
+ - Speed up and reduce memory usage of Commit#repo_changes, Repository#expire_avatar_cache and IrkerWorker
+ - Add unfold links for Side-by-Side view. !5415 (Tim Masliuchenko)
+ - Adds support for pending invitation project members importing projects
+ - Add pipeline visualization/graph on pipeline page
+ - Update devise initializer to turn on changed password notification emails. !5648 (tombell)
+ - Avoid to show the original password field when password is automatically set. !5712 (duduribeiro)
+ - Fix importing GitLab projects with an invalid MR source project
+ - Sort folders with submodules in Files view !5521
+ - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
+ - Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
+ - Add pipelines tab to merge requests
+ - Fix notification_service argument error of declined invitation emails
+ - Fix a memory leak caused by Banzai::Filter::SanitizationFilter
+ - Speed up todos queries by limiting the projects set we join with
+ - Ensure file editing in UI does not overwrite commited changes without warning user
+ - Eliminate unneeded calls to Repository#blob_at when listing commits with no path
+ - Update gitlab_git gem to 10.4.7
+ - Simplify SQL queries of marking a todo as done
+
+v 8.10.7
+ - Upgrade Hamlit to 2.6.1. !5873
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
+v 8.10.6
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+ - Restore "Largest repository" sort option on Admin > Projects page. !5797
+ - Fix privilege escalation via project export.
+ - Require administrator privileges to perform a project import.
+
+v 8.10.5
+ - Add a data migration to fix some missing timestamps in the members table. !5670
+ - Revert the "Defend against 'Host' header injection" change in the source NGINX templates. !5706
+ - Cache project count for 5 minutes to reduce DB load. !5746 & !5754
+
+v 8.10.4
+ - Don't close referenced upstream issues from a forked project.
+ - Fixes issue with dropdowns `enter` key not working correctly. !5544
+ - Fix Import/Export project import not working in HA mode. !5618
+ - Fix Import/Export error checking versions. !5638
+
+v 8.10.3
+ - Fix Import/Export issue importing milestones and labels not associated properly. !5426
+ - Fix timing problems running imports on production. !5523
+ - Add a log message when a project is scheduled for destruction for debugging. !5540
+ - Fix hooks missing on imported GitLab projects. !5549
+ - Properly abort a merge when merge conflicts occur. !5569
+ - Fix importer for GitHub Pull Requests when a branch was removed. !5573
+ - Ignore invalid IPs in X-Forwarded-For when trusted proxies are configured. !5584
+ - Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
+ - Fix label already exist error message in the right sidebar.
+
+v 8.10.2
+ - User can now search branches by name. !5144
+ - Page is now properly rendered after committing the first file and creating the first branch. !5399
+ - Add branch or tag icon to ref in builds page. !5434
+ - Fix backup restore. !5459
+ - Use project ID in repository cache to prevent stale data from persisting across projects. !5460
+ - Fix issue with autocomplete search not working with enter key. !5466
+ - Add iid to MR API response. !5468
+ - Disable MySQL foreign key checks before dropping all tables. !5472
+ - Ensure relative paths for video are rewritten as we do for images. !5474
+ - Ensure current user can retry a build before showing the 'Retry' button. !5476
+ - Add ENV variable to skip repository storages validations. !5478
+ - Added `*.js.es6 gitlab-language=javascript` to `.gitattributes`. !5486
+ - Don't show comment button in gutter of diffs on MR discussion tab. !5493
+ - Rescue Rugged::OSError (lock exists) when creating references. !5497
+ - Fix expand all diffs button in compare view. !5500
+ - Show release notes in tags list. !5503
+ - Fix a bug where forking a project from a repository storage to another would fail. !5509
+ - Fix missing schema update for `20160722221922`. !5512
+ - Update `gitlab-shell` version to 3.2.1 in the 8.9->8.10 update guide. !5516
+
+v 8.10.1
+ - Refactor repository storages documentation. !5428
+ - Gracefully handle case when keep-around references are corrupted or exist already. !5430
+ - Add detailed info on storage path mountpoints. !5437
+ - Fix Error 500 when creating Wiki pages with hyphens or spaces. !5444
+ - Fix bug where replies to commit notes displayed in the MR discussion tab wouldn't show up on the commit page. !5446
+ - Ignore invalid trusted proxies in X-Forwarded-For header. !5454
+ - Add links to the real markdown.md file for all GFM examples. !5458
+
+v 8.10.0
+ - Fix profile activity heatmap to show correct day name (eanplatter)
+ - Speed up ExternalWikiHelper#get_project_wiki_path
+ - Expose {should,force}_remove_source_branch (Ben Boeckel)
+ - Add the functionality to be able to rename a file. !5049
+ - Disable PostgreSQL statement timeout during migrations
+ - Fix projects dropdown loading performance with a simplified api cal. !5113
- Fix commit builds API, return all builds for all pipelines for given commit. !4849
- Replace Haml with Hamlit to make view rendering faster. !3666
+ - Refresh the branch cache after `git gc` runs
+ - Allow to disable request access button on projects/groups
- Refactor repository paths handling to allow multiple git mount points
+ - Optimize system note visibility checking by memoizing the visible reference count. !5070
- Add Application Setting to configure default Repository Path for new projects
+ - Delete award emoji when deleting a user
+ - Remove pinTo from Flash and make inline flash messages look nicer. !4854 (winniehell)
+ - Add an API for downloading latest successful build from a particular branch or tag. !5347
+ - Avoid data-integrity issue when cleaning up repository archive cache.
+ - Add link to profile to commit avatar. !5163 (winniehell)
- Wrap code blocks on Activies and Todos page. !4783 (winniehell)
- - Align flash messages with left side of page content !4959 (winniehell)
- - Display last commit of deleted branch in push events !4699 (winniehell)
- - Escape file extension when parsing search results !5141 (winniehell)
+ - Align flash messages with left side of page content. !4959 (winniehell)
+ - Display tooltip for "Copy to Clipboard" button. !5164 (winniehell)
+ - Use default cursor for table header of project files. !5165 (winniehell)
+ - Store when and yaml variables in builds table
+ - Display last commit of deleted branch in push events. !4699 (winniehell)
+ - Escape file extension when parsing search results. !5141 (winniehell)
+ - Add "passing with warnings" to the merge request pipeline possible statuses, this happens when builds that allow failures have failed. !5004
+ - Add image border in Markdown preview. !5162 (winniehell)
- Apply the trusted_proxies config to the rack request object for use with rack_attack
+ - Added the ability to block sign ups using a domain blacklist. !5259
+ - Upgrade to Rails 4.2.7. !5236
+ - Extend exposed environment variables for CI builds
+ - Deprecate APIs "projects/:id/keys/...". Use "projects/:id/deploy_keys/..." instead
+ - Add API "deploy_keys" for admins to get all deploy keys
+ - Allow to pull code with deploy key from public projects
+ - Use limit parameter rather than hardcoded value in `ldap:check` rake task (Mike Ricketts)
- Add Sidekiq queue duration to transaction metrics.
- - Add a new column `artifacts_size` to table `ci_builds` !4964
+ - Add a new column `artifacts_size` to table `ci_builds`. !4964
- Let Workhorse serve format-patch diffs
+ - Display tooltip for mentioned users and groups. !5261 (winniehell)
+ - Allow build email service to be tested
+ - Added day name to contribution calendar tooltips
+ - Refactor user authorization check for a single project to avoid querying all user projects
+ - Make images fit to the size of the viewport. !4810
+ - Fix check for New Branch button on Issue page. !4630 (winniehell)
+ - Fix GFM autocomplete not working on wiki pages
+ - Fixed enter key not triggering click on first row when searching in a dropdown
+ - Updated dropdowns in issuable form to use new GitLab dropdown style
- Make images fit to the size of the viewport !4810
- Fix check for New Branch button on Issue page !4630 (winniehell)
- Fix MR-auto-close text added to description. !4836
- - Fix issue, preventing users w/o push access to sort tags !5105 (redetection)
+ - Support U2F devices in Firefox. !5177
+ - Fix issue, preventing users w/o push access to sort tags. !5105 (redetection)
- Add Spring EmojiOne updates.
- - Add syntax for multiline blockquote using `>>>` fence !3954
+ - Added Rake task for tracking deployments. !5320
+ - Fix fetching LFS objects for private CI projects
+ - Add the new 2016 Emoji! Adds 72 new emoji including bacon, facepalm, and selfie. !5237
+ - Add syntax for multiline blockquote using `>>>` fence. !3954
- Fix viewing notification settings when a project is pending deletion
+ - Updated compare dropdown menus to use GL dropdown
+ - Redirects back to issue after clicking login link
+ - Eager load award emoji on notes
+ - Allow to define manual actions/builds on Pipelines and Environments
- Fix pagination when sorting by columns with lots of ties (like priority)
- - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times !5020
+ - The Markdown reference parsers now re-use query results to prevent running the same queries multiple times. !5020
- Updated project header design
+ - Issuable collapsed assignee tooltip is now the users name
+ - Fix compare view not changing code view rendering style
- Exclude email check from the standard health check
- - Updated layout for Projects, Groups, Users on Admin area !4424
+ - Updated layout for Projects, Groups, Users on Admin area. !4424
- Fix changing issue state columns in milestone view
+ - Update health_check gem to version 2.1.0
- Add notification settings dropdown for groups
+ - Render inline diffs for multiple changed lines following eachother
- Wildcards for protected branches. !4665
- Allow importing from Github using Personal Access Tokens. (Eric K Idema)
- - API: Todos !3188 (Robert Schilling)
- - API: Expose shared groups for projects and shared projects for groups !5050 (Robert Schilling)
+ - API: Expose `due_date` for issues (Robert Schilling)
+ - API: Todos. !3188 (Robert Schilling)
+ - API: Expose shared groups for projects and shared projects for groups. !5050 (Robert Schilling)
+ - API: Expose `developers_can_push` and `developers_can_merge` for branches. !5208 (Robert Schilling)
- Add "Enabled Git access protocols" to Application Settings
- - Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
+ - Diffs will create button/diff form on demand no on server side
+ - Reduce size of HTML used by diff comment forms
+ - Protected branches have a "Developers can Merge" setting. !4892 (original implementation by Mathias Vestergaard)
+ - Fix user creation with stronger minimum password requirements. !4054 (nathan-pmt)
- Only show New Snippet button to users that can create snippets.
- PipelinesFinder uses git cache data
+ - Track a user who created a pipeline
+ - Actually render old and new sections of parallel diff next to each other
- Throttle the update of `project.pushes_since_gc` to 1 minute.
+ - Allow expanding and collapsing files in diff view. !4990
+ - Collapse large diffs by default (!4990)
+ - Fix mentioned users list on diff notes
+ - Add support for inline videos in GitLab Flavored Markdown. !5215 (original implementation by Eric Hayes)
+ - Fix creation of deployment on build that is retried, redeployed or rollback
+ - Don't parse Rinku returned value to DocFragment when it didn't change the original html string.
- Check for conflicts with existing Project's wiki path when creating a new project.
- Show last push widget in upstream after push to fork
+ - Fix stage status shown for pipelines
+ - Cache todos pending/done dashboard query counts.
- Don't instantiate a git tree on Projects show default view
- Bump Rinku to 2.0.0
- Remove unused front-end variable -> default_issues_tracker
+ - ObjectRenderer retrieve renderer content using Rails.cache.read_multi
- Better caching of git calls on ProjectsController#show.
- Avoid to retrieve MR closes_issues as much as possible.
- - Add API endpoint for a group issues !4520 (mahcsig)
- - Add Bugzilla integration !4930 (iamtjg)
+ - Hide project name in project activities. !5068 (winniehell)
+ - Add API endpoint for a group issues. !4520 (mahcsig)
+ - Add Bugzilla integration. !4930 (iamtjg)
+ - Fix new snippet style bug (elliotec)
- Instrument Rinku usage
+ - Be explicit to define merge request discussion variables
+ - Use cache for todos counter calling TodoService
- Metrics for Rouge::Plugins::Redcarpet and Rouge::Formatters::HTMLGitlab
- RailsCache metris now includes fetch_hit/fetch_miss and read_hit/read_miss info.
- Allow [ci skip] to be in any case and allow [skip ci]. !4785 (simon_w)
+ - Made project list visibility icon fixed width
- Set import_url validation to be more strict
- Memoize MR merged/closed events retrieval
- Don't render discussion notes when requesting diff tab through AJAX
- Add basic system information like memory and disk usage to the admin panel
- Don't garbage collect commits that have related DB records like comments
+ - Allow to setup event by channel on slack service
- More descriptive message for git hooks and file locks
+ - Aliases of award emoji should be stored as original name. !5060 (dixpac)
- Handle custom Git hook result in GitLab UI
+ - Allow to access Container Registry for Public and Internal projects
- Allow '?', or '&' for label names
+ - Support redirected blobs for Container Registry integration
- Fix importer for GitHub Pull Requests when a branch was reused across Pull Requests
- Add date when user joined the team on the member page
- Fix 404 redirect after validation fails importing a GitLab project
- - Added setting to set new users by default as external !4545 (Dravere)
- - Add min value for project limit field on user's form !3622 (jastkand)
- - Add reminder to not paste private SSH keys !4399 (Ingo Blechschmidt)
+ - Added setting to set new users by default as external. !4545 (Dravere)
+ - Add min value for project limit field on user's form. !3622 (jastkand)
+ - Reset project pushes_since_gc when we enqueue the git gc call
+ - Add reminder to not paste private SSH keys. !4399 (Ingo Blechschmidt)
+ - Collapsed diffs lines/size don't acumulate to overflow diffs.
+ - Remove duplicate `description` field in `MergeRequest` entities (Ben Boeckel)
+ - Style of import project buttons were fixed in the new project page. !5183 (rdemirbay)
+ - Fix GitHub client requests when rate limit is disabled
+ - Optimistic locking for Issues and Merge Requests (Title and description overriding prevention)
+ - Redesign Builds and Pipelines pages
+ - Change status color and icon for running builds
+ - Fix commenting issue in side by side diff view for unchanged lines
+ - Fix markdown rendering for: consecutive labels references, label references that begin with a digit or contains `.`
+ - Project export filename now includes the project and namespace path
+ - Fix last update timestamp on issues not preserved on gitlab.com and project imports
+ - Fix issues importing projects from EE to CE
+ - Fix creating group with space in group path
+ - Improve cron_jobs loading error messages. !5318 / !5360
+ - Prevent toggling sidebar when clipboard icon clicked
+ - Create Todos for Issue author when assign or mention himself (Katarzyna Kobierska)
+ - Limit the number of retries on error to 3 for exporting projects
+ - Allow empty repositories on project import/export
+ - Render only commit message title in builds (Katarzyna Kobierska Ula Budziszewska)
+ - Allow bulk (un)subscription from issues in issue index
+ - Fix MR diff encoding issues exporting GitLab projects
+ - Move builds settings out of project settings and rename Pipelines
+ - Add builds badge to Pipelines settings page
+ - Export and import avatar as part of project import/export
+ - Fix migration corrupting import data for old version upgrades
+ - Show tooltip on GitLab export link in new project page
+
+v 8.9.8
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
+v 8.9.7
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+ - Require administrator privileges to perform a project import.
+
+v 8.9.6
+ - Fix importing of events under notes for GitLab projects. !5154
+ - Fix log statements in import/export. !5129
+ - Fix commit avatar alignment in compare view. !5128
+ - Fix broken migration in MySQL. !5005
+ - Overwrite Host and X-Forwarded-Host headers in NGINX !5213
+ - Keeps issue number when importing from Gitlab.com
+ - Add Pending tab for Builds (Katarzyna Kobierska, Urszula Budziszewska)
+
+v 8.9.7 (unreleased)
+ - Fix import_data wrongly saved as a result of an invalid import_url
+
+v 8.9.6
+ - Fix importing of events under notes for GitLab projects
v 8.9.5
- Add more debug info to import/export and memory killer. !5108
@@ -150,6 +547,7 @@ v 8.9.1
- Remove width restriction for logo on sign-in page. !4888
- Bump gitlab_git to 10.2.3 to fix false truncated warnings with ISO-8559 files. !4884
- Apply selected value as label. !4886
+ - Change Retry to Re-deploy on Deployments page
- Fix temp file being deleted after the request while importing a GitLab project. !4894
- Fix pagination when sorting by columns with lots of ties (like priority)
- Implement Subresource Integrity for CSS and JavaScript assets. This prevents malicious assets from loading in the case of a CDN compromise.
@@ -223,6 +621,7 @@ v 8.9.0
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix race condition on merge when build succeeds
+ - Added shortcut to focus filter search fields and added documentation #18120
- Links from a wiki page to other wiki pages should be rewritten as expected
- Add option to project to only allow merge requests to be merged if the build succeeds (Rui Santos)
- Added navigation shortcuts to the project pipelines, milestones, builds and forks page. !4393
@@ -312,6 +711,12 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
+v 8.8.9
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
+v 8.8.8
+ - Upgrade Rails to 4.2.7.1 for security fixes. !5781
+
v 8.8.7
- Fix privilege escalation issue with OAuth external users.
- Ensure references to private repos aren't shown to logged-out users.
@@ -2184,8 +2589,6 @@ v 7.7.0
- Fixes for edit comments: drag-n-drop images, preview mode, selecting images, save & update
- Remove password strength indicator
-
-
v 7.6.0
- Fork repository to groups
- New rugged version
@@ -2611,13 +3014,13 @@ v 6.5.0
- Files API supports base64 encoded content (sponsored by O'Reilly Media)
- Added support for Go's repository retrieval (Bruno Albuquerque)
-v6.4.3
+v 6.4.3
- Don't use unicorn worker killer if PhusionPassenger is defined
-v6.4.2
+v 6.4.2
- Fixed wrong behaviour of script/upgrade.rb
-v6.4.1
+v 6.4.1
- Fixed bug with repository rename
- Fixed bug with project transfer
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f4472214778..c457af2ae6f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -41,6 +41,8 @@ abbreviation.
If you have read this guide and want to know how the GitLab [core team]
operates please see [the GitLab contributing process](PROCESS.md).
+- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+
## Contributor license agreement
By submitting code as an individual you agree to the
@@ -127,7 +129,7 @@ request that potentially fixes it.
### Feature proposals
-To create a feature proposal for CE and CI, open an issue on the
+To create a feature proposal for CE, open an issue on the
[issue tracker of CE][ce-tracker].
For feature proposals for EE, open an issue on the
@@ -142,15 +144,7 @@ code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones
might be edited to make them small and simple.
-You are encouraged to use the template below for feature proposals.
-
-```
-## Description including problem, use cases, benefits, and/or goals
-
-## Proposal
-
-## Links / references
-```
+Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it can be helpful to create a mockup first.
If you want to create something yourself, consider opening an issue first to
@@ -163,55 +157,11 @@ submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
-Please submit bugs using the following template in the issue description area.
+Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
The text in the parenthesis is there to help you with what to include. Omit it
when submitting the actual issue. You can copy-paste it and then edit as you
see fit.
-```
-## Summary
-
-(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
-
-## Steps to reproduce
-
-(How one can reproduce the issue - this is very important)
-
-## Expected behavior
-
-(What you should see instead)
-
-## Relevant logs and/or screenshots
-
-(Paste any relevant logs - please use code blocks (```) to format console output,
-logs, and code as it's very hard to read otherwise.)
-
-## Output of checks
-
-### Results of GitLab Application Check
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:check SANITIZE=true)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
-
-(we will only investigate if the tests are passing)
-
-### Results of GitLab Environment Info
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:env:info)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
-
-## Possible fixes
-
-(If you can, link to the line of code that might be responsible for the problem)
-
-```
-
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
@@ -333,6 +283,10 @@ request is as follows:
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
+1. If your merge request adds one or more migrations, make sure to execute all
+ migrations on a fresh database before the MR is reviewed. If the review leads
+ to large changes in the MR, do this again once the review is complete.
+1. For more complex migrations, write tests.
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
@@ -380,7 +334,8 @@ description area. Copy-paste it to retain the markdown format.
1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
- exposing a bug in existing code)
+ exposing a bug in existing code). Every new class should have corresponding
+ unit tests, even if the class is exercised at a higher level, such as a feature test.
1. If you suspect a failing CI build is unrelated to your contribution, you may
try and restart the failing CI job or ask a developer to fix the
aforementioned failing test
@@ -458,8 +413,10 @@ merge request:
- multi-line method chaining style **Option B**: dot `.` on previous line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
+1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md)
-1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
+1. [JavaScript (ES6)](https://github.com/airbnb/javascript)
+1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/master/es5)
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
@@ -529,6 +486,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[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"
[gitlab-design]: https://gitlab.com/gitlab-org/gitlab-design
[free Antetype viewer (Mac OSX only)]: https://itunes.apple.com/us/app/antetype-viewer/id824152298?mt=12
[`gitlab8.atype` file]: https://gitlab.com/gitlab-org/gitlab-design/tree/master/current/
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 944880fa15e..18091983f59 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.2.0
+3.4.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 879be8a98fc..b4d6d12101f 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.7
+0.7.11
diff --git a/Gemfile b/Gemfile
index f1fef4caf76..194379dd687 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-gem 'rails', '4.2.6'
+gem 'rails', '4.2.7.1'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
@@ -9,6 +9,7 @@ gem 'responders', '~> 2.0'
# Specify a sprockets version due to increased performance
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
gem 'sprockets', '~> 3.6.0'
+gem 'sprockets-es6'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -19,7 +20,7 @@ gem 'pg', '~> 0.18.2', group: :postgres
# Authentication libraries
gem 'devise', '~> 4.0'
-gem 'doorkeeper', '~> 4.0'
+gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
@@ -52,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.2'
+gem 'gitlab_git', '~> 10.4.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -61,14 +62,14 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
-gem 'gollum-lib', '~> 4.1.0', require: false
+gem 'gollum-lib', '~> 4.2', require: false
gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
# Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.13.0'
+gem 'grape', '~> 0.15.0'
gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
@@ -76,7 +77,7 @@ gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
gem 'kaminari', '~> 0.17.0'
# HAML
-gem 'hamlit', '~> 2.5'
+gem 'hamlit', '~> 2.6.1'
# Files attachments
gem 'carrierwave', '~> 0.10.0'
@@ -105,7 +106,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0'
gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
-gem 'github-markup', '~> 1.3.1'
+gem 'github-markup', '~> 1.4'
gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~>3.6'
@@ -113,7 +114,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 1.11'
+gem 'rouge', '~> 2.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -153,7 +154,7 @@ gem 'settingslogic', '~> 2.0.9'
# Misc
-gem 'version_sorter', '~> 2.0.0'
+gem 'version_sorter', '~> 2.1.0'
# Cache
gem 'redis-rails', '~> 4.0.0'
@@ -162,9 +163,6 @@ gem 'redis-rails', '~> 4.0.0'
gem 'redis', '~> 3.2'
gem 'connection_pool', '~> 2.0'
-# Campfire integration
-gem 'tinder', '~> 1.10.0'
-
# HipChat integration
gem 'hipchat', '~> 1.5.0'
@@ -203,7 +201,7 @@ gem 'licensee', '~> 8.0.0'
gem 'rack-attack', '~> 4.3.1'
# Ace editor
-gem 'ace-rails-ap', '~> 4.0.2'
+gem 'ace-rails-ap', '~> 4.1.0'
# Keyboard shortcuts
gem 'mousetrap-rails', '~> 1.4.6'
@@ -211,7 +209,8 @@ gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
gem 'charlock_holmes', '~> 0.7.3'
-# Parse duration
+# Parse time & duration
+gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.0'
@@ -223,8 +222,8 @@ gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.6.1'
-gem 'gemojione', '~> 2.6'
-gem 'gon', '~> 6.0.1'
+gem 'gemojione', '~> 3.0'
+gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
gem 'jquery-rails', '~> 4.1.0'
gem 'jquery-ui-rails', '~> 5.0.0'
@@ -252,7 +251,7 @@ group :development do
gem 'letter_opener_web', '~> 1.3.0'
gem 'rerun', '~> 0.11.0'
- gem 'bullet', '~> 5.0.0', require: false
+ gem 'bullet', '~> 5.2.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
gem 'web-console', '~> 2.0'
@@ -274,7 +273,7 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
- gem 'database_cleaner', '~> 1.4.0'
+ gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.6.0'
gem 'rspec-rails', '~> 3.5.0'
gem 'rspec-retry', '~> 0.4.5'
@@ -299,10 +298,10 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.40.0', require: false
+ gem 'rubocop', '~> 0.41.2', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
- gem 'simplecov', '~> 0.11.0', require: false
+ gem 'simplecov', '0.12.0', require: false
gem 'flog', '~> 4.3.2', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -316,6 +315,7 @@ end
group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
+ gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 1.21.0'
gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack', '~> 1.3.6'
@@ -325,7 +325,7 @@ group :production do
gem 'gitlab_meta', '7.0'
end
-gem 'newrelic_rpm', '~> 3.14'
+gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
@@ -333,6 +333,8 @@ gem 'mail_room', '~> 0.8'
gem 'email_reply_parser', '~> 0.5.8'
+gem 'ruby-prof', '~> 0.15.9'
+
## CI
gem 'activerecord-session_store', '~> 1.0.0'
gem 'nested_form', '~> 0.3.2'
@@ -344,8 +346,8 @@ gem 'oauth2', '~> 1.2.0'
gem 'paranoia', '~> 2.0'
# Health check
-gem 'health_check', '~> 1.5.1'
+gem 'health_check', '~> 2.1.0'
# System information
-gem 'vmstat', '~> 2.1.0'
+gem 'vmstat', '~> 2.2'
gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 721ab9ddc5d..0c28975060c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,35 +2,35 @@ GEM
remote: https://rubygems.org/
specs:
RedCloth (4.3.2)
- ace-rails-ap (4.0.2)
- actionmailer (4.2.6)
- actionpack (= 4.2.6)
- actionview (= 4.2.6)
- activejob (= 4.2.6)
+ ace-rails-ap (4.1.0)
+ actionmailer (4.2.7.1)
+ actionpack (= 4.2.7.1)
+ actionview (= 4.2.7.1)
+ activejob (= 4.2.7.1)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (4.2.6)
- actionview (= 4.2.6)
- activesupport (= 4.2.6)
+ actionpack (4.2.7.1)
+ actionview (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (4.2.6)
- activesupport (= 4.2.6)
+ actionview (4.2.7.1)
+ activesupport (= 4.2.7.1)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (4.2.6)
- activesupport (= 4.2.6)
+ activejob (4.2.7.1)
+ activesupport (= 4.2.7.1)
globalid (>= 0.3.0)
- activemodel (4.2.6)
- activesupport (= 4.2.6)
+ activemodel (4.2.7.1)
+ activesupport (= 4.2.7.1)
builder (~> 3.1)
- activerecord (4.2.6)
- activemodel (= 4.2.6)
- activesupport (= 4.2.6)
+ activerecord (4.2.7.1)
+ activemodel (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
arel (~> 6.0)
activerecord-session_store (1.0.0)
actionpack (>= 4.0, < 5.1)
@@ -38,7 +38,7 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 1.5.2, < 3)
railties (>= 4.0, < 5.1)
- activesupport (4.2.6)
+ activesupport (4.2.7.1)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
@@ -58,8 +58,8 @@ GEM
faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0)
asciidoctor (1.5.3)
- ast (2.2.0)
- attr_encrypted (3.0.1)
+ ast (2.3.0)
+ attr_encrypted (3.0.3)
encryptor (~> 3.0.0)
attr_required (1.0.0)
autoprefixer-rails (6.2.3)
@@ -85,6 +85,10 @@ GEM
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)
+ execjs (~> 2.0)
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
@@ -100,9 +104,9 @@ GEM
brakeman (3.3.2)
browser (2.2.0)
builder (3.2.2)
- bullet (5.0.0)
+ bullet (5.2.0)
activesupport (>= 3.0.0)
- uniform_notifier (~> 1.9.0)
+ uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0)
bundler (~> 1.2)
thor (~> 0.18)
@@ -124,6 +128,7 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.3)
+ chronic (0.10.2)
chronic_duration (0.10.6)
numerizer (~> 0.1.1)
chunky_png (1.3.5)
@@ -149,11 +154,11 @@ GEM
d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
- database_cleaner (1.4.1)
+ database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
- default_value_for (3.0.1)
- activerecord (>= 3.2.0, < 5.0)
+ default_value_for (3.0.2)
+ activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.1.1)
@@ -171,7 +176,7 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.7)
docile (1.1.5)
- doorkeeper (4.0.0)
+ doorkeeper (4.2.0)
railties (>= 4.2)
dropzonejs-rails (0.7.2)
rails (> 3.1)
@@ -255,7 +260,7 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.6.1)
+ gemojione (3.0.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
@@ -264,7 +269,7 @@ GEM
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
rugged (>= 0.23.0b)
- github-markup (1.3.3)
+ github-markup (1.4.0)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
gitlab-grit (>= 2.4.1)
@@ -274,7 +279,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.2.3)
+ gitlab_git (10.4.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -285,26 +290,26 @@ GEM
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.3)
- globalid (0.3.6)
+ globalid (0.3.7)
activesupport (>= 4.1.0)
- gollum-grit_adapter (1.0.0)
+ gollum-grit_adapter (1.0.1)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.1.0)
- github-markup (~> 1.3.3)
+ gollum-lib (4.2.1)
+ github-markup (~> 1.4.0)
gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4)
- rouge (~> 1.9)
+ rouge (~> 2.0)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
gollum-rugged_adapter (0.4.2)
mime-types (>= 1.15)
rugged (~> 0.24.0, >= 0.21.3)
- gon (6.0.1)
+ gon (6.1.0)
actionpack (>= 3.0)
json
multi_json
request_store (>= 1.0)
- grape (0.13.0)
+ grape (0.15.0)
activesupport
builder
hashie (>= 2.1.0)
@@ -317,13 +322,13 @@ GEM
grape-entity (0.4.8)
activesupport
multi_json (>= 1.3.2)
- hamlit (2.5.0)
+ hamlit (2.6.1)
temple (~> 0.7.6)
thor
tilt
hashie (3.4.3)
- health_check (1.5.1)
- rails (>= 2.3.0)
+ health_check (2.1.0)
+ rails (>= 4.0)
hipchat (1.5.2)
httparty
mimemagic
@@ -331,11 +336,10 @@ GEM
activesupport (>= 2)
nokogiri (~> 1.4)
htmlentities (4.3.4)
- http_parser.rb (0.5.3)
httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.7.0.1)
+ httpclient (2.8.2)
i18n (0.7.0)
ice_nine (0.11.1)
influxdb (0.2.3)
@@ -353,6 +357,8 @@ GEM
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
+ json-schema (2.6.2)
+ addressable (~> 2.3.8)
jwt (1.5.4)
kaminari (0.17.0)
actionpack (>= 3.0.0)
@@ -400,7 +406,7 @@ GEM
nested_form (0.3.2)
net-ldap (0.12.1)
net-ssh (3.0.1)
- newrelic_rpm (3.14.1.311)
+ newrelic_rpm (3.16.0.318)
nokogiri (1.6.8)
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
@@ -473,7 +479,7 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.3.1.0)
+ parser (2.3.1.2)
ast (~> 2.2)
pg (0.18.4)
pkg-config (1.1.7)
@@ -505,7 +511,7 @@ GEM
rack-cors (0.4.0)
rack-mount (0.8.3)
rack (>= 1.0.0)
- rack-oauth2 (1.2.1)
+ rack-oauth2 (1.2.3)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
@@ -515,16 +521,16 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.2.6)
- actionmailer (= 4.2.6)
- actionpack (= 4.2.6)
- actionview (= 4.2.6)
- activejob (= 4.2.6)
- activemodel (= 4.2.6)
- activerecord (= 4.2.6)
- activesupport (= 4.2.6)
+ rails (4.2.7.1)
+ actionmailer (= 4.2.7.1)
+ actionpack (= 4.2.7.1)
+ actionview (= 4.2.7.1)
+ activejob (= 4.2.7.1)
+ activemodel (= 4.2.7.1)
+ activerecord (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.2.6)
+ railties (= 4.2.7.1)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
@@ -534,9 +540,9 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- railties (4.2.6)
- actionpack (= 4.2.6)
- activesupport (= 4.2.6)
+ railties (4.2.7.1)
+ actionpack (= 4.2.7.1)
+ activesupport (= 4.2.7.1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
@@ -571,14 +577,14 @@ GEM
redis-store (~> 1.1.0)
redis-store (1.1.7)
redis (>= 2.2)
- request_store (1.3.0)
+ request_store (1.3.1)
rerun (0.11.0)
listen (~> 3.0)
responders (2.1.1)
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (1.11.0)
+ rouge (2.0.5)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -606,8 +612,8 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.40.0)
- parser (>= 2.3.1.0, < 3.0)
+ rubocop (0.41.2)
+ parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7)
@@ -616,6 +622,7 @@ GEM
rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
+ ruby-prof (0.15.9)
ruby-progressbar (1.8.1)
ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
@@ -667,10 +674,9 @@ GEM
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0)
- simple_oauth (0.1.9)
- simplecov (0.11.2)
+ simplecov (0.12.0)
docile (~> 1.1.0)
- json (~> 1.8)
+ json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
sinatra (1.4.7)
@@ -697,9 +703,13 @@ GEM
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
- sprockets (3.6.2)
+ sprockets (3.6.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
+ sprockets-es6 (0.9.0)
+ babel-source (>= 5.8.11)
+ babel-transpiler
+ sprockets (>= 3.0.0)
sprockets-rails (3.1.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
@@ -733,21 +743,8 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
- tinder (1.10.1)
- eventmachine (~> 1.0)
- faraday (~> 0.9.0)
- faraday_middleware (~> 0.9)
- hashie (>= 1.0)
- json (~> 1.8.0)
- mime-types
- multi_json (~> 1.7)
- twitter-stream (~> 0.1)
turbolinks (2.5.3)
coffee-rails
- twitter-stream (0.1.16)
- eventmachine (>= 0.12.8)
- http_parser.rb (~> 0.5.1)
- simple_oauth (~> 0.1.4)
tzinfo (1.2.2)
thread_safe (~> 0.1)
u2f (0.2.1)
@@ -758,7 +755,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.2)
- unicode-display_width (1.0.5)
+ unicode-display_width (1.1.0)
unicorn (4.9.0)
kgio (~> 2.6)
rack
@@ -766,16 +763,16 @@ GEM
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
unicorn (>= 4, < 6)
- uniform_notifier (1.9.0)
+ uniform_notifier (1.10.0)
uuid (2.3.8)
macaddr (~> 1.0)
- version_sorter (2.0.0)
+ version_sorter (2.1.0)
virtus (1.0.5)
axiom-types (~> 0.1)
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- vmstat (2.1.0)
+ vmstat (2.2.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
@@ -802,7 +799,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
- ace-rails-ap (~> 4.0.2)
+ ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
@@ -821,24 +818,25 @@ DEPENDENCIES
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0)
browser (~> 2.2)
- bullet (~> 5.0.0)
+ 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)
charlock_holmes (~> 0.7.3)
+ chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
connection_pool (~> 2.0)
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
- database_cleaner (~> 1.4.0)
+ database_cleaner (~> 1.5.0)
default_value_for (~> 3.0.0)
devise (~> 4.0)
devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3)
- doorkeeper (~> 4.0)
+ doorkeeper (~> 4.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
@@ -857,20 +855,20 @@ DEPENDENCIES
foreman (~> 0.78.0)
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
- gemojione (~> 2.6)
+ gemojione (~> 3.0)
github-linguist (~> 4.7.0)
- github-markup (~> 1.3.1)
+ github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.2)
+ gitlab_git (~> 10.4.7)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
- gollum-lib (~> 4.1.0)
+ gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
- gon (~> 6.0.1)
- grape (~> 0.13.0)
+ gon (~> 6.1.0)
+ grape (~> 0.15.0)
grape-entity (~> 0.4.2)
- hamlit (~> 2.5)
- health_check (~> 1.5.1)
+ hamlit (~> 2.6.1)
+ health_check (~> 2.1.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
@@ -879,6 +877,7 @@ DEPENDENCIES
jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0)
+ json-schema (~> 2.6.2)
jwt
kaminari (~> 0.17.0)
knapsack (~> 1.11.0)
@@ -893,7 +892,7 @@ DEPENDENCIES
mysql2 (~> 0.3.16)
nested_form (~> 0.3.2)
net-ssh (~> 3.0.1)
- newrelic_rpm (~> 3.14)
+ newrelic_rpm (~> 3.16)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.2.0)
octokit (~> 4.3.0)
@@ -920,7 +919,7 @@ DEPENDENCIES
rack-attack (~> 4.3.1)
rack-cors (~> 0.4.0)
rack-oauth2 (~> 1.2.1)
- rails (= 4.2.6)
+ rails (= 4.2.7.1)
rails-deprecated_sanitizer (~> 1.0.3)
rainbow (~> 2.1.0)
rblineprof (~> 0.3.6)
@@ -933,13 +932,14 @@ DEPENDENCIES
request_store (~> 1.3.0)
rerun (~> 0.11.0)
responders (~> 2.0)
- rouge (~> 1.11)
+ rouge (~> 2.0)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.40.0)
+ rubocop (~> 0.41.2)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
+ ruby-prof (~> 0.15.9)
sanitize (~> 2.0)
sass-rails (~> 5.0.0)
scss_lint (~> 0.47.0)
@@ -952,7 +952,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.0)
sidekiq-cron (~> 0.4.0)
- simplecov (~> 0.11.0)
+ simplecov (= 0.12.0)
sinatra (~> 1.4.4)
six (~> 0.2.0)
slack-notifier (~> 1.2.0)
@@ -963,6 +963,7 @@ DEPENDENCIES
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 3.6.0)
+ sprockets-es6
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
task_list (~> 1.0.2)
@@ -970,7 +971,6 @@ DEPENDENCIES
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
- tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
@@ -978,9 +978,9 @@ DEPENDENCIES
unf (~> 0.1.4)
unicorn (~> 4.9.0)
unicorn-worker-killer (~> 0.4.2)
- version_sorter (~> 2.0.0)
+ version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
- vmstat (~> 2.1.0)
+ vmstat (~> 2.2)
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
diff --git a/MAINTENANCE.md b/MAINTENANCE.md
index d3d36670693..1efb2a35f6d 100644
--- a/MAINTENANCE.md
+++ b/MAINTENANCE.md
@@ -1,15 +1,35 @@
# GitLab Maintenance Policy
-GitLab is a fast moving and evolving project. We currently don't have the resources to support many releases concurrently. We support exactly one stable release at any given time.
+GitLab follows the [Semantic Versioning](http://semver.org/) for its releases:
+`(Major).(Minor).(Patch)` in a [pragmatic way].
-GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: `(Major).(Minor).(Patch)` in a [pragmatic way](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e).
+- **Major version**: Whenever there is something significant or any backwards
+ incompatible changes are introduced to the public API.
+- **Minor version**: When new, backwards compatible functionality is introduced
+ to the public API or a minor feature is introduced, or when a set of smaller
+ features is rolled out.
+- **Patch number**: When backwards compatible bug fixes are introduced that fix
+ incorrect behavior.
-- **Major version**: Whenever there is something significant or any backwards incompatible changes are introduced to the public API.
-- **Minor version**: When new, backwards compatible functionality is introduced to the public API or a minor feature is introduced, or when a set of smaller features is rolled out.
-- **Patch number**: When backwards compatible bug fixes are introduced that fix incorrect behavior.
+The current stable release will receive security patches and bug fixes
+(eg. `8.9.0` -> `8.9.1`). Feature releases will mark the next supported stable
+release where the minor version is increased numerically by increments of one
+(eg. `8.9 -> 8.10`).
-The current stable release will receive security patches and bug fixes (eg. `5.0` -> `5.0.1`). Feature releases will mark the next supported stable release where the minor version is increased numerically by increments of one (eg. `5.0 -> 5.1`).
+Our current policy is to support one stable release at any given time, but for
+medium-level security issues, we may consider [backporting to the previous two
+monthly releases][rel-sec].
-We encourage everyone to run the latest stable release to ensure that you can easily upgrade to the most secure and feature rich GitLab experience. In order to make sure you can easily run the most recent stable release, we are working hard to keep the update process simple and reliable.
+We encourage everyone to run the latest stable release to ensure that you can
+easily upgrade to the most secure and feature-rich GitLab experience. In order
+to make sure you can easily run the most recent stable release, we are working
+hard to keep the update process simple and reliable.
-More information about the release procedures can be found in the doc/release directory.
+More information about the release procedures can be found in our
+[release-tools documentation][rel]. You may also want to read our
+[Responsible Disclosure Policy][disclosure].
+
+[rel-sec]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/security.md#backporting
+[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/
+[disclosure]: https://about.gitlab.com/disclosure/
+[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e
diff --git a/PROCESS.md b/PROCESS.md
index fe3a963110d..8e1a3f7360f 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -8,6 +8,8 @@ treatment, etc.). And so that maintainers know what to expect from contributors
(use the latest version, ensure that the issue is addressed, friendly treatment,
etc.).
+- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+
## Common actions
### Issue team
diff --git a/README.md b/README.md
index fee93d5f9c3..3df8bfa04c7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +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)
+[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
## Canonical source
@@ -69,7 +70,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
-- Ruby (MRI) 2.1
+- Ruby (MRI) 2.3
- Git 2.7.4+
- Redis 2.8+
- MySQL or PostgreSQL
diff --git a/VERSION b/VERSION
index 213504430f3..8a5bbc87ef9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.10.0-pre
+8.12.0-pre
diff --git a/app/assets/images/bg-header.png b/app/assets/images/bg-header.png
deleted file mode 100644
index 639271c6faf..00000000000
--- a/app/assets/images/bg-header.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/bg_fallback.png b/app/assets/images/bg_fallback.png
deleted file mode 100644
index 5c55bc79dec..00000000000
--- a/app/assets/images/bg_fallback.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/chosen-sprite.png b/app/assets/images/chosen-sprite.png
deleted file mode 100644
index 3d936b07d44..00000000000
--- a/app/assets/images/chosen-sprite.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/diff_note_add.png b/app/assets/images/diff_note_add.png
deleted file mode 100644
index 0084422e330..00000000000
--- a/app/assets/images/diff_note_add.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
index 6bacb0e92b6..6f1a34a5591 100644
--- a/app/assets/images/emoji.png
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/emoji@2x.png b/app/assets/images/emoji@2x.png
index 99588b56616..dc9cae1d44c 100644
--- a/app/assets/images/emoji@2x.png
+++ b/app/assets/images/emoji@2x.png
Binary files differ
diff --git a/app/assets/images/icon-search.png b/app/assets/images/icon-search.png
deleted file mode 100644
index 3c1c146541d..00000000000
--- a/app/assets/images/icon-search.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/icon_sprite.png b/app/assets/images/icon_sprite.png
deleted file mode 100644
index 2e7a5023398..00000000000
--- a/app/assets/images/icon_sprite.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/images.png b/app/assets/images/images.png
deleted file mode 100644
index bd60de994c4..00000000000
--- a/app/assets/images/images.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg
new file mode 100644
index 00000000000..ad89d684d94
--- /dev/null
+++ b/app/assets/images/koding-logo.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
+ <g fill="#d6d7d9">
+ <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
+ <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
+ <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/assets/images/move.png b/app/assets/images/move.png
deleted file mode 100644
index 6a0567f8f25..00000000000
--- a/app/assets/images/move.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/progress_bar.gif b/app/assets/images/progress_bar.gif
deleted file mode 100644
index c3d43fa40b2..00000000000
--- a/app/assets/images/progress_bar.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png
deleted file mode 100644
index 52ad11ab7a1..00000000000
--- a/app/assets/images/slider_handles.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png
deleted file mode 100644
index c6b6c8d9521..00000000000
--- a/app/assets/images/switch_icon.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif
deleted file mode 100644
index 1a1c9c15ec7..00000000000
--- a/app/assets/images/trans_bg.gif
+++ /dev/null
Binary files differ
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
new file mode 100644
index 00000000000..151455ce4a3
--- /dev/null
+++ b/app/assets/javascripts/LabelManager.js
@@ -0,0 +1,110 @@
+(function() {
+ this.LabelManager = (function() {
+ LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
+
+ function LabelManager(opts) {
+ var ref, ref1, ref2;
+ if (opts == null) {
+ opts = {};
+ }
+ this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
+ this.prioritizedLabels.sortable({
+ items: 'li',
+ placeholder: 'list-placeholder',
+ axis: 'y',
+ update: this.onPrioritySortUpdate.bind(this)
+ });
+ this.bindEvents();
+ }
+
+ LabelManager.prototype.bindEvents = function() {
+ return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+ };
+
+ LabelManager.prototype.onTogglePriorityClick = function(e) {
+ var $btn, $label, $tooltip, _this, action;
+ e.preventDefault();
+ _this = e.data;
+ $btn = $(e.currentTarget);
+ $label = $("#" + ($btn.data('domId')));
+ action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
+ $tooltip.tooltip('destroy');
+ return _this.toggleLabelPriority($label, action);
+ };
+
+ LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
+ var $from, $target, _this, url, xhr;
+ if (persistState == null) {
+ persistState = true;
+ }
+ _this = this;
+ url = $label.find('.js-toggle-priority').data('url');
+ $target = this.prioritizedLabels;
+ $from = this.otherLabels;
+ if (action === 'remove') {
+ $target = this.otherLabels;
+ $from = this.prioritizedLabels;
+ }
+ if ($from.find('li').length === 1) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ if (!$target.find('li').length) {
+ $target.find('.empty-message').addClass('hidden');
+ }
+ $label.detach().appendTo($target);
+ if (!persistState) {
+ return;
+ }
+ if (action === 'remove') {
+ xhr = $.ajax({
+ url: url,
+ type: 'DELETE'
+ });
+ if (!$from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ } else {
+ xhr = this.savePrioritySort($label, action);
+ }
+ return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+ };
+
+ LabelManager.prototype.onPrioritySortUpdate = function() {
+ var xhr;
+ xhr = this.savePrioritySort();
+ return xhr.fail(function() {
+ return new Flash(this.errorMessage, 'alert');
+ });
+ };
+
+ LabelManager.prototype.savePrioritySort = function() {
+ return $.post({
+ url: this.prioritizedLabels.data('url'),
+ data: {
+ label_ids: this.getSortedLabelsIds()
+ }
+ });
+ };
+
+ LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) {
+ var action;
+ action = originalAction === 'remove' ? 'add' : 'remove';
+ this.toggleLabelPriority($label, action, false);
+ return new Flash(this.errorMessage, 'alert');
+ };
+
+ LabelManager.prototype.getSortedLabelsIds = function() {
+ var sortedIds;
+ sortedIds = [];
+ this.prioritizedLabels.find('li').each(function() {
+ return sortedIds.push($(this).data('id'));
+ });
+ return sortedIds;
+ };
+
+ return LabelManager;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee
deleted file mode 100644
index 6d8faba40d7..00000000000
--- a/app/assets/javascripts/LabelManager.js.coffee
+++ /dev/null
@@ -1,92 +0,0 @@
-class @LabelManager
- errorMessage: 'Unable to update label prioritization at this time'
-
- constructor: (opts = {}) ->
- # Defaults
- {
- @togglePriorityButton = $('.js-toggle-priority')
- @prioritizedLabels = $('.js-prioritized-labels')
- @otherLabels = $('.js-other-labels')
- } = opts
-
- @prioritizedLabels.sortable(
- items: 'li'
- placeholder: 'list-placeholder'
- axis: 'y'
- update: @onPrioritySortUpdate.bind(@)
- )
-
- @bindEvents()
-
- bindEvents: ->
- @togglePriorityButton.on 'click', @, @onTogglePriorityClick
-
- onTogglePriorityClick: (e) ->
- e.preventDefault()
- _this = e.data
- $btn = $(e.currentTarget)
- $label = $("##{$btn.data('domId')}")
- action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
-
- # Make sure tooltip will hide
- $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
- $tooltip.tooltip 'destroy'
-
- _this.toggleLabelPriority($label, action)
-
- toggleLabelPriority: ($label, action, persistState = true) ->
- _this = @
- url = $label.find('.js-toggle-priority').data 'url'
-
- $target = @prioritizedLabels
- $from = @otherLabels
-
- # Optimistic update
- if action is 'remove'
- $target = @otherLabels
- $from = @prioritizedLabels
-
- if $from.find('li').length is 1
- $from.find('.empty-message').removeClass('hidden')
-
- if not $target.find('li').length
- $target.find('.empty-message').addClass('hidden')
-
- $label.detach().appendTo($target)
-
- # Return if we are not persisting state
- return unless persistState
-
- if action is 'remove'
- xhr = $.ajax url: url, type: 'DELETE'
-
- # Restore empty message
- $from.find('.empty-message').removeClass('hidden') unless $from.find('li').length
- else
- xhr = @savePrioritySort($label, action)
-
- xhr.fail @rollbackLabelPosition.bind(@, $label, action)
-
- onPrioritySortUpdate: ->
- xhr = @savePrioritySort()
-
- xhr.fail ->
- new Flash(@errorMessage, 'alert')
-
- savePrioritySort: () ->
- $.post
- url: @prioritizedLabels.data('url')
- data:
- label_ids: @getSortedLabelsIds()
-
- rollbackLabelPosition: ($label, originalAction)->
- action = if originalAction is 'remove' then 'add' else 'remove'
- @toggleLabelPriority($label, action, false)
-
- new Flash(@errorMessage, 'alert')
-
- getSortedLabelsIds: ->
- sortedIds = []
- @prioritizedLabels.find('li').each ->
- sortedIds.push $(@).data 'id'
- sortedIds
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
new file mode 100644
index 00000000000..2fe46b9fd06
--- /dev/null
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -0,0 +1,38 @@
+((global) => {
+ const MAX_MESSAGE_LENGTH = 500;
+ const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
+
+ class AbuseReports {
+ constructor() {
+ $(MESSAGE_CELL_SELECTOR).each(this.truncateLongMessage);
+ $(document)
+ .off('click', MESSAGE_CELL_SELECTOR)
+ .on('click', MESSAGE_CELL_SELECTOR, this.toggleMessageTruncation);
+ }
+
+ truncateLongMessage() {
+ const $messageCellElement = $(this);
+ const reportMessage = $messageCellElement.text();
+ if (reportMessage.length > MAX_MESSAGE_LENGTH) {
+ $messageCellElement.data('original-message', reportMessage);
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(global.text.truncate(reportMessage, MAX_MESSAGE_LENGTH));
+ }
+ }
+
+ toggleMessageTruncation() {
+ const $messageCellElement = $(this);
+ const originalMessage = $messageCellElement.data('original-message');
+ if (!originalMessage) return;
+ if ($messageCellElement.data('message-truncated') === 'true') {
+ $messageCellElement.data('message-truncated', 'false');
+ $messageCellElement.text(originalMessage);
+ } else {
+ $messageCellElement.data('message-truncated', 'true');
+ $messageCellElement.text(`${originalMessage.substr(0, (MAX_MESSAGE_LENGTH - 3))}...`);
+ }
+ }
+ }
+
+ global.AbuseReports = AbuseReports;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
new file mode 100644
index 00000000000..5ea6086ab77
--- /dev/null
+++ b/app/assets/javascripts/activities.js
@@ -0,0 +1,40 @@
+(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', '#activity'));
+ };
+
+ Activities.prototype.reloadActivities = function() {
+ $(".content_list").html('');
+ return Pager.init(20, true);
+ };
+
+ Activities.prototype.toggleFilter = function(sender) {
+ var event_filters, filter;
+ $('.event-filter .active').removeClass("active");
+ event_filters = $.cookie("event_filter");
+ filter = sender.attr("id").split("_")[0];
+ $.cookie("event_filter", (event_filters !== filter ? filter : ""), {
+ path: gon.relative_url_root || '/'
+ });
+ if (event_filters !== filter) {
+ return sender.closest('li').toggleClass("active");
+ }
+ };
+
+ return Activities;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
deleted file mode 100644
index ed5a5d0260c..00000000000
--- a/app/assets/javascripts/activities.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-class @Activities
- constructor: ->
- Pager.init 20, true, false, @updateTooltips
- $(".event-filter-link").on "click", (event) =>
- event.preventDefault()
- @toggleFilter($(event.currentTarget))
- @reloadActivities()
-
- updateTooltips: ->
- gl.utils.localTimeAgo($('.js-timeago', '#activity'))
-
- reloadActivities: ->
- $(".content_list").html ''
- Pager.init 20, true
-
-
- toggleFilter: (sender) ->
- $('.event-filter .active').removeClass "active"
- event_filters = $.cookie("event_filter")
- filter = sender.attr("id").split("_")[0]
- $.cookie "event_filter", (if event_filters isnt filter then filter else ""), { path: '/' }
-
- if event_filters isnt filter
- sender.closest('li').toggleClass "active"
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
new file mode 100644
index 00000000000..f8460beb5d2
--- /dev/null
+++ b/app/assets/javascripts/admin.js
@@ -0,0 +1,64 @@
+(function() {
+ this.Admin = (function() {
+ function Admin() {
+ var modal, showBlacklistType;
+ $('input#user_force_random_password').on('change', function(elem) {
+ var elems;
+ elems = $('#user_password, #user_password_confirmation');
+ if ($(this).attr('checked')) {
+ return elems.val('').attr('disabled', true);
+ } else {
+ return elems.removeAttr('disabled');
+ }
+ });
+ $('body').on('click', '.js-toggle-colors-link', function(e) {
+ e.preventDefault();
+ return $('.js-toggle-colors-container').toggle();
+ });
+ $('.log-tabs a').click(function(e) {
+ e.preventDefault();
+ return $(this).tab('show');
+ });
+ $('.log-bottom').click(function(e) {
+ var visible_log;
+ e.preventDefault();
+ visible_log = $(".file-content:visible");
+ return visible_log.animate({
+ scrollTop: visible_log.find('ol').height()
+ }, "fast");
+ });
+ modal = $('.change-owner-holder');
+ $('.change-owner-link').bind("click", function(e) {
+ e.preventDefault();
+ $(this).hide();
+ return modal.show();
+ });
+ $('.change-owner-cancel-link').bind("click", function(e) {
+ e.preventDefault();
+ modal.hide();
+ return $('.change-owner-link').show();
+ });
+ $('li.project_member').bind('ajax:success', function() {
+ return Turbolinks.visit(location.href);
+ });
+ $('li.group_member').bind('ajax:success', function() {
+ return Turbolinks.visit(location.href);
+ });
+ showBlacklistType = function() {
+ if ($("input[name='blacklist_type']:checked").val() === 'file') {
+ $('.blacklist-file').show();
+ return $('.blacklist-raw').hide();
+ } else {
+ $('.blacklist-file').hide();
+ return $('.blacklist-raw').show();
+ }
+ };
+ $("input[name='blacklist_type']").click(showBlacklistType);
+ showBlacklistType();
+ }
+
+ return Admin;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee
deleted file mode 100644
index b2b8e1b7ffb..00000000000
--- a/app/assets/javascripts/admin.js.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-class @Admin
- constructor: ->
- $('input#user_force_random_password').on 'change', (elem) ->
- elems = $('#user_password, #user_password_confirmation')
-
- if $(@).attr 'checked'
- elems.val('').attr 'disabled', true
- else
- elems.removeAttr 'disabled'
-
- $('body').on 'click', '.js-toggle-colors-link', (e) ->
- e.preventDefault()
- $('.js-toggle-colors-container').toggle()
-
- $('.log-tabs a').click (e) ->
- e.preventDefault()
- $(this).tab('show')
-
- $('.log-bottom').click (e) ->
- e.preventDefault()
- visible_log = $(".file-content:visible")
- visible_log.animate({ scrollTop: visible_log.find('ol').height() }, "fast")
-
- modal = $('.change-owner-holder')
-
- $('.change-owner-link').bind "click", (e) ->
- e.preventDefault()
- $(this).hide()
- modal.show()
-
- $('.change-owner-cancel-link').bind "click", (e) ->
- e.preventDefault()
- modal.hide()
- $('.change-owner-link').show()
-
- $('li.project_member').bind 'ajax:success', ->
- Turbolinks.visit(location.href)
-
- $('li.group_member').bind 'ajax:success', ->
- Turbolinks.visit(location.href)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
new file mode 100644
index 00000000000..84b292e59c6
--- /dev/null
+++ b/app/assets/javascripts/api.js
@@ -0,0 +1,145 @@
+(function() {
+ this.Api = {
+ groupsPath: "/api/:version/groups.json",
+ groupPath: "/api/:version/groups/:id.json",
+ namespacesPath: "/api/:version/namespaces.json",
+ groupProjectsPath: "/api/:version/groups/:id/projects.json",
+ projectsPath: "/api/:version/projects.json?simple=true",
+ labelsPath: "/api/:version/projects/:id/labels",
+ licensePath: "/api/:version/licenses/:key",
+ gitignorePath: "/api/:version/gitignores/:key",
+ gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
+ issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
+
+ group: function(group_id, callback) {
+ var url = Api.buildUrl(Api.groupPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ data: {
+ private_token: gon.api_token
+ },
+ dataType: "json"
+ }).done(function(group) {
+ return callback(group);
+ });
+ },
+ groups: function(query, skip_ldap, callback) {
+ var url = Api.buildUrl(Api.groupsPath);
+ return $.ajax({
+ url: url,
+ data: {
+ private_token: gon.api_token,
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(groups) {
+ return callback(groups);
+ });
+ },
+ namespaces: function(query, callback) {
+ var url = Api.buildUrl(Api.namespacesPath);
+ return $.ajax({
+ url: url,
+ data: {
+ private_token: gon.api_token,
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(namespaces) {
+ return callback(namespaces);
+ });
+ },
+ projects: function(query, order, callback) {
+ var url = Api.buildUrl(Api.projectsPath);
+ return $.ajax({
+ url: url,
+ data: {
+ private_token: gon.api_token,
+ search: query,
+ order_by: order,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ newLabel: function(project_id, data, callback) {
+ var url = Api.buildUrl(Api.labelsPath)
+ .replace(':id', project_id);
+ data.private_token = gon.api_token;
+ return $.ajax({
+ url: url,
+ type: "POST",
+ data: data,
+ dataType: "json"
+ }).done(function(label) {
+ return callback(label);
+ }).error(function(message) {
+ return callback(message.responseJSON);
+ });
+ },
+ groupProjects: function(group_id, query, callback) {
+ var url = Api.buildUrl(Api.groupProjectsPath)
+ .replace(':id', group_id);
+ return $.ajax({
+ url: url,
+ data: {
+ private_token: gon.api_token,
+ search: query,
+ per_page: 20
+ },
+ dataType: "json"
+ }).done(function(projects) {
+ return callback(projects);
+ });
+ },
+ licenseText: function(key, data, callback) {
+ var url = Api.buildUrl(Api.licensePath)
+ .replace(':key', key);
+ return $.ajax({
+ url: url,
+ data: data
+ }).done(function(license) {
+ return callback(license);
+ });
+ },
+ gitignoreText: function(key, callback) {
+ var url = Api.buildUrl(Api.gitignorePath)
+ .replace(':key', key);
+ return $.get(url, function(gitignore) {
+ return callback(gitignore);
+ });
+ },
+ gitlabCiYml: function(key, callback) {
+ var url = Api.buildUrl(Api.gitlabCiYmlPath)
+ .replace(':key', key);
+ return $.get(url, function(file) {
+ return callback(file);
+ });
+ },
+ issueTemplate: function(namespacePath, projectPath, key, type, callback) {
+ var url = Api.buildUrl(Api.issuableTemplatePath)
+ .replace(':key', key)
+ .replace(':type', type)
+ .replace(':project_path', projectPath)
+ .replace(':namespace_path', namespacePath);
+ $.ajax({
+ url: url,
+ dataType: 'json'
+ }).done(function(file) {
+ callback(null, file);
+ }).error(callback);
+ },
+ buildUrl: function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root + url;
+ }
+ return url.replace(':version', gon.api_version);
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
deleted file mode 100644
index cf46f15a156..00000000000
--- a/app/assets/javascripts/api.js.coffee
+++ /dev/null
@@ -1,122 +0,0 @@
-@Api =
- groupsPath: "/api/:version/groups.json"
- groupPath: "/api/:version/groups/:id.json"
- namespacesPath: "/api/:version/namespaces.json"
- groupProjectsPath: "/api/:version/groups/:id/projects.json"
- projectsPath: "/api/:version/projects.json"
- labelsPath: "/api/:version/projects/:id/labels"
- licensePath: "/api/:version/licenses/:key"
- gitignorePath: "/api/:version/gitignores/:key"
- gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
-
- group: (group_id, callback) ->
- url = Api.buildUrl(Api.groupPath)
- url = url.replace(':id', group_id)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- dataType: "json"
- ).done (group) ->
- callback(group)
-
- # Return groups list. Filtered by query
- # Only active groups retrieved
- groups: (query, skip_ldap, callback) ->
- url = Api.buildUrl(Api.groupsPath)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- per_page: 20
- dataType: "json"
- ).done (groups) ->
- callback(groups)
-
- # Return namespaces list. Filtered by query
- namespaces: (query, callback) ->
- url = Api.buildUrl(Api.namespacesPath)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- per_page: 20
- dataType: "json"
- ).done (namespaces) ->
- callback(namespaces)
-
- # Return projects list. Filtered by query
- projects: (query, order, callback) ->
- url = Api.buildUrl(Api.projectsPath)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- order_by: order
- per_page: 20
- dataType: "json"
- ).done (projects) ->
- callback(projects)
-
- newLabel: (project_id, data, callback) ->
- url = Api.buildUrl(Api.labelsPath)
- url = url.replace(':id', project_id)
-
- data.private_token = gon.api_token
- $.ajax(
- url: url
- type: "POST"
- data: data
- dataType: "json"
- ).done (label) ->
- callback(label)
- .error (message) ->
- callback(message.responseJSON)
-
- # Return group projects list. Filtered by query
- groupProjects: (group_id, query, callback) ->
- url = Api.buildUrl(Api.groupProjectsPath)
- url = url.replace(':id', group_id)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- per_page: 20
- dataType: "json"
- ).done (projects) ->
- callback(projects)
-
- # Return text for a specific license
- licenseText: (key, data, callback) ->
- url = Api.buildUrl(Api.licensePath).replace(':key', key)
-
- $.ajax(
- url: url
- data: data
- ).done (license) ->
- callback(license)
-
- gitignoreText: (key, callback) ->
- url = Api.buildUrl(Api.gitignorePath).replace(':key', key)
-
- $.get url, (gitignore) ->
- callback(gitignore)
-
- gitlabCiYml: (key, callback) ->
- url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
-
- $.get url, (file) ->
- callback(file)
-
- buildUrl: (url) ->
- url = gon.relative_url_root + url if gon.relative_url_root?
- return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
new file mode 100644
index 00000000000..43a679501a7
--- /dev/null
+++ b/app/assets/javascripts/application.js
@@ -0,0 +1,329 @@
+/*= require jquery2 */
+/*= require jquery-ui/autocomplete */
+/*= require jquery-ui/datepicker */
+/*= require jquery-ui/draggable */
+/*= require jquery-ui/effect-highlight */
+/*= require jquery-ui/sortable */
+/*= require jquery_ujs */
+/*= require jquery.cookie */
+/*= require jquery.endless-scroll */
+/*= require jquery.highlight */
+/*= require jquery.waitforimages */
+/*= require jquery.atwho */
+/*= require jquery.scrollTo */
+/*= require jquery.turbolinks */
+/*= require turbolinks */
+/*= require autosave */
+/*= require bootstrap/affix */
+/*= require bootstrap/alert */
+/*= require bootstrap/button */
+/*= require bootstrap/collapse */
+/*= require bootstrap/dropdown */
+/*= require bootstrap/modal */
+/*= require bootstrap/scrollspy */
+/*= require bootstrap/tab */
+/*= require bootstrap/transition */
+/*= require bootstrap/tooltip */
+/*= require bootstrap/popover */
+/*= require select2 */
+/*= require underscore */
+/*= require dropzone */
+/*= require mousetrap */
+/*= require mousetrap/pause */
+/*= require shortcuts */
+/*= require shortcuts_navigation */
+/*= require shortcuts_dashboard_navigation */
+/*= require shortcuts_issuable */
+/*= require shortcuts_network */
+/*= require jquery.nicescroll */
+/*= require date.format */
+/*= require_directory ./behaviors */
+/*= require_directory ./blob */
+/*= require_directory ./templates */
+/*= require_directory ./commit */
+/*= require_directory ./extensions */
+/*= require_directory ./lib/utils */
+/*= require_directory ./u2f */
+/*= require_directory . */
+/*= require fuzzaldrin-plus */
+
+(function() {
+ window.slugify = function(text) {
+ return text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase();
+ };
+
+ window.ajaxGet = function(url) {
+ return $.ajax({
+ type: "GET",
+ url: url,
+ dataType: "script"
+ });
+ };
+
+ window.split = function(val) {
+ return val.split(/,\s*/);
+ };
+
+ window.extractLast = function(term) {
+ return split(term).pop();
+ };
+
+ window.rstrip = function(val) {
+ if (val) {
+ return val.replace(/\s+$/, '');
+ } else {
+ return val;
+ }
+ };
+
+ window.disableButtonIfEmptyField = function(field_selector, button_selector) {
+ var closest_submit, field;
+ field = $(field_selector);
+ closest_submit = field.closest('form').find(button_selector);
+ if (rstrip(field.val()) === "") {
+ closest_submit.disable();
+ }
+ return field.on('input', function() {
+ if (rstrip($(this).val()) === "") {
+ return closest_submit.disable();
+ } else {
+ return closest_submit.enable();
+ }
+ });
+ };
+
+ window.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 = rstrip($(this).val()) !== "" || !$(this).attr('required');
+ });
+ if (filled) {
+ return closest_submit.enable();
+ } else {
+ return closest_submit.disable();
+ }
+ };
+ updateButtons();
+ return form.keyup(updateButtons);
+ };
+
+ window.sanitize = function(str) {
+ return str.replace(/<(?:.|\n)*?>/gm, '');
+ };
+
+ window.unbindEvents = function() {
+ return $(document).off('scroll');
+ };
+
+ window.shiftWindow = function() {
+ return scrollBy(0, -100);
+ };
+
+ document.addEventListener("page:fetch", unbindEvents);
+
+ window.addEventListener("hashchange", shiftWindow);
+
+ window.onload = function() {
+ if (location.hash) {
+ return setTimeout(shiftWindow, 100);
+ }
+ };
+
+ $(function() {
+ var $body, $document, $sidebarGutterToggle, $window, bootstrapBreakpoint, checkInitialSidebarSize, fitSidebarForSize, flash;
+ $document = $(document);
+ $window = $(window);
+ $body = $('body');
+ gl.utils.preventDisabledButtons();
+ bootstrapBreakpoint = bp.getBreakpointSize();
+ $(".nav-sidebar").niceScroll({
+ cursoropacitymax: '0.4',
+ cursorcolor: '#FFF',
+ cursorborder: "1px solid #FFF"
+ });
+ $(".js-select-on-focus").on("focusin", function() {
+ return $(this).select().one('mouseup', function(e) {
+ return e.preventDefault();
+ });
+ });
+ $('.remove-row').bind('ajax:success', function() {
+ $(this).tooltip('destroy')
+ .closest('li')
+ .fadeOut();
+ });
+ $('.js-remove-tr').bind('ajax:before', function() {
+ return $(this).hide();
+ });
+ $('.js-remove-tr').bind('ajax:success', function() {
+ return $(this).closest('tr').fadeOut();
+ });
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true
+ });
+ $('.js-select2').bind('select2-close', function() {
+ return setTimeout((function() {
+ $('.select2-container-active').removeClass('select2-container-active');
+ return $(':focus').blur();
+ }), 1);
+ });
+ $body.tooltip({
+ selector: '.has-tooltip, [data-toggle="tooltip"]',
+ placement: function(_, el) {
+ var $el;
+ $el = $(el);
+ return $el.data('placement') || 'bottom';
+ }
+ });
+ $('.trigger-submit').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ if ((flash = $(".flash-container")).length > 0) {
+ flash.click(function() {
+ return $(this).fadeOut();
+ });
+ flash.show();
+ }
+ $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
+ var buttons;
+ buttons = $('[type="submit"]', this);
+ switch (e.type) {
+ case 'ajax:beforeSend':
+ case 'submit':
+ return buttons.disable();
+ default:
+ return buttons.enable();
+ }
+ });
+ $(document).ajaxError(function(e, xhrObj, xhrSetting, xhrErrorText) {
+ var ref;
+ if (xhrObj.status === 401) {
+ return new Flash('You need to be logged in.', 'alert');
+ } else if ((ref = xhrObj.status) === 404 || ref === 500) {
+ return new Flash('Something went wrong on our end.', 'alert');
+ }
+ });
+ $('.account-box').hover(function() {
+ return $(this).toggleClass('hover');
+ });
+ $document.on('click', '.diff-content .js-show-suppressed-diff', function() {
+ var $container;
+ $container = $(this).parent();
+ $container.next('table').show();
+ return $container.remove();
+ });
+ $('.navbar-toggle').on('click', function() {
+ $('.header-content .title').toggle();
+ $('.header-content .header-logo').toggle();
+ $('.header-content .navbar-collapse').toggle();
+ return $('.navbar-toggle').toggleClass('active');
+ });
+ $body.on("click", ".js-toggle-diff-comments", function(e) {
+ var $this = $(this);
+ $this.toggleClass('active');
+ var notesHolders = $this.closest('.diff-file').find('.notes_holder');
+ if ($this.hasClass('active')) {
+ notesHolders.show();
+ } else {
+ notesHolders.hide();
+ }
+ return e.preventDefault();
+ });
+ $document.off("click", '.js-confirm-danger');
+ $document.on("click", '.js-confirm-danger', function(e) {
+ var btn, form, text;
+ e.preventDefault();
+ btn = $(e.target);
+ text = btn.data("confirm-danger-message");
+ form = btn.closest("form");
+ return new ConfirmDangerModal(form, text);
+ });
+ $document.on('click', 'button', function() {
+ return $(this).blur();
+ });
+ $('input[type="search"]').each(function() {
+ var $this;
+ $this = $(this);
+ $this.attr('value', $this.val());
+ });
+ $document.off('keyup', 'input[type="search"]').on('keyup', 'input[type="search"]', function(e) {
+ var $this;
+ $this = $(this);
+ return $this.attr('value', $this.val());
+ });
+ $sidebarGutterToggle = $('.js-sidebar-toggle');
+ $document.off('breakpoint:change').on('breakpoint:change', function(e, breakpoint) {
+ var $gutterIcon;
+ if (breakpoint === 'sm' || breakpoint === 'xs') {
+ $gutterIcon = $sidebarGutterToggle.find('i');
+ if ($gutterIcon.hasClass('fa-angle-double-right')) {
+ return $sidebarGutterToggle.trigger('click');
+ }
+ }
+ });
+ fitSidebarForSize = function() {
+ var oldBootstrapBreakpoint;
+ oldBootstrapBreakpoint = bootstrapBreakpoint;
+ bootstrapBreakpoint = bp.getBreakpointSize();
+ if (bootstrapBreakpoint !== oldBootstrapBreakpoint) {
+ return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+ }
+ };
+ checkInitialSidebarSize = function() {
+ bootstrapBreakpoint = bp.getBreakpointSize();
+ if (bootstrapBreakpoint === "xs" || "sm") {
+ return $document.trigger('breakpoint:change', [bootstrapBreakpoint]);
+ }
+ };
+ $window.off("resize.app").on("resize.app", function(e) {
+ return fitSidebarForSize();
+ });
+ gl.awardsHandler = new AwardsHandler();
+ checkInitialSidebarSize();
+ new Aside();
+ if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
+ $.cookie('pin_nav', 'false', {
+ path: gon.relative_url_root || '/',
+ expires: 365 * 10
+ });
+ $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
+ $('.navbar-fixed-top').removeClass('header-pinned-nav');
+ }
+ $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
+ var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText;
+ e.preventDefault();
+ $pinBtn = $(e.currentTarget);
+ $page = $('.page-with-sidebar');
+ $topNav = $('.navbar-fixed-top');
+ $tooltip = $("#" + ($pinBtn.attr('aria-describedby')));
+ doPinNav = !$page.is('.page-sidebar-pinned');
+ tooltipText = 'Pin navigation';
+ $(this).toggleClass('is-active');
+ if (doPinNav) {
+ $page.addClass('page-sidebar-pinned');
+ $topNav.addClass('header-pinned-nav');
+ } else {
+ $tooltip.remove();
+ $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
+ $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
+ }
+ $.cookie('pin_nav', doPinNav, {
+ path: gon.relative_url_root || '/',
+ expires: 365 * 10
+ });
+ if ($.cookie('pin_nav') === 'true' || doPinNav) {
+ tooltipText = 'Unpin navigation';
+ }
+ $tooltip.find('.tooltip-inner').text(tooltipText);
+ return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
+ });
+
+ // Custom time ago
+ gl.utils.shortTimeAgo($('.js-short-timeago'));
+ });
+}).call(this);
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
deleted file mode 100644
index 64da503c35f..00000000000
--- a/app/assets/javascripts/application.js.coffee
+++ /dev/null
@@ -1,312 +0,0 @@
-# This is a manifest file that'll be compiled into including all the files listed below.
-# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-# be included in the compiled file accessible from http://example.com/assets/application.js
-# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-# the compiled file.
-#
-#= require jquery2
-#= require jquery-ui/autocomplete
-#= require jquery-ui/datepicker
-#= require jquery-ui/draggable
-#= require jquery-ui/effect-highlight
-#= require jquery-ui/sortable
-#= require jquery_ujs
-#= require jquery.cookie
-#= require jquery.endless-scroll
-#= require jquery.highlight
-#= require jquery.waitforimages
-#= require jquery.atwho
-#= require jquery.scrollTo
-#= require jquery.turbolinks
-#= require turbolinks
-#= require autosave
-#= require bootstrap/affix
-#= require bootstrap/alert
-#= require bootstrap/button
-#= require bootstrap/collapse
-#= require bootstrap/dropdown
-#= require bootstrap/modal
-#= require bootstrap/scrollspy
-#= require bootstrap/tab
-#= require bootstrap/transition
-#= require bootstrap/tooltip
-#= require bootstrap/popover
-#= require select2
-#= require ace/ace
-#= require ace/ext-searchbox
-#= require underscore
-#= require dropzone
-#= require mousetrap
-#= require mousetrap/pause
-#= require shortcuts
-#= require shortcuts_navigation
-#= require shortcuts_dashboard_navigation
-#= require shortcuts_issuable
-#= require shortcuts_network
-#= require jquery.nicescroll
-#= require date.format
-#= require_directory ./behaviors
-#= require_directory ./blob
-#= require_directory ./ci
-#= require_directory ./commit
-#= require_directory ./extensions
-#= require_directory ./lib/utils
-#= require_directory ./u2f
-#= require_directory .
-#= require fuzzaldrin-plus
-#= require u2f
-
-window.slugify = (text) ->
- text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
-
-window.ajaxGet = (url) ->
- $.ajax({type: "GET", url: url, dataType: "script"})
-
-window.split = (val) ->
- return val.split( /,\s*/ )
-
-window.extractLast = (term) ->
- return split( term ).pop()
-
-window.rstrip = (val) ->
- return if val then val.replace(/\s+$/, '') else val
-
-# Disable button if text field is empty
-window.disableButtonIfEmptyField = (field_selector, button_selector) ->
- field = $(field_selector)
- closest_submit = field.closest('form').find(button_selector)
-
- closest_submit.disable() if rstrip(field.val()) is ""
-
- field.on 'input', ->
- if rstrip($(@).val()) is ""
- closest_submit.disable()
- else
- closest_submit.enable()
-
-# Disable button if any input field with given selector is empty
-window.disableButtonIfAnyEmptyField = (form, form_selector, button_selector) ->
- closest_submit = form.find(button_selector)
- updateButtons = ->
- filled = true
- form.find('input').filter(form_selector).each ->
- filled = rstrip($(this).val()) != "" || !$(this).attr('required')
-
- if filled
- closest_submit.enable()
- else
- closest_submit.disable()
-
- updateButtons()
- form.keyup(updateButtons)
-
-window.sanitize = (str) ->
- return str.replace(/<(?:.|\n)*?>/gm, '')
-
-window.unbindEvents = ->
- $(document).off('scroll')
-
-window.shiftWindow = ->
- scrollBy 0, -100
-
-document.addEventListener("page:fetch", unbindEvents)
-
-window.addEventListener "hashchange", shiftWindow
-
-window.onload = ->
- # Scroll the window to avoid the topnav bar
- # https://github.com/twitter/bootstrap/issues/1768
- if location.hash
- setTimeout shiftWindow, 100
-
-$ ->
-
- $document = $(document)
- $window = $(window)
- $body = $('body')
-
- gl.utils.preventDisabledButtons()
- bootstrapBreakpoint = bp.getBreakpointSize()
-
- $(".nav-sidebar").niceScroll(cursoropacitymax: '0.4', cursorcolor: '#FFF', cursorborder: "1px solid #FFF")
-
- # Click a .js-select-on-focus field, select the contents
- $(".js-select-on-focus").on "focusin", ->
- # Prevent a mouseup event from deselecting the input
- $(this).select().one 'mouseup', (e) ->
- e.preventDefault()
-
- $('.remove-row').bind 'ajax:success', ->
- $(this).closest('li').fadeOut()
-
- $('.js-remove-tr').bind 'ajax:before', ->
- $(this).hide()
-
- $('.js-remove-tr').bind 'ajax:success', ->
- $(this).closest('tr').fadeOut()
-
- # Initialize select2 selects
- $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true)
-
- # Close select2 on escape
- $('.js-select2').bind 'select2-close', ->
- setTimeout ( ->
- $('.select2-container-active').removeClass('select2-container-active')
- $(':focus').blur()
- ), 1
-
- # Initialize tooltips
- $body.tooltip(
- selector: '.has-tooltip, [data-toggle="tooltip"]'
- placement: (_, el) ->
- $el = $(el)
- $el.data('placement') || 'bottom'
- )
-
- # Form submitter
- $('.trigger-submit').on 'change', ->
- $(@).parents('form').submit()
-
- gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true)
-
- # Flash
- if (flash = $(".flash-container")).length > 0
- flash.click -> $(@).fadeOut()
- flash.show()
-
- # Disable form buttons while a form is submitting
- $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
- buttons = $('[type="submit"]', @)
-
- switch e.type
- when 'ajax:beforeSend', 'submit'
- buttons.disable()
- else
- buttons.enable()
-
- $(document).ajaxError (e, xhrObj, xhrSetting, xhrErrorText) ->
-
- if xhrObj.status is 401
- new Flash 'You need to be logged in.', 'alert'
-
- else if xhrObj.status in [ 404, 500 ]
- new Flash 'Something went wrong on our end.', 'alert'
-
-
- # Show/Hide the profile menu when hovering the account box
- $('.account-box').hover -> $(@).toggleClass('hover')
-
- # Commit show suppressed diff
- $document.on 'click', '.diff-content .js-show-suppressed-diff', ->
- $container = $(@).parent()
- $container.next('table').show()
- $container.remove()
-
- $('.navbar-toggle').on 'click', ->
- $('.header-content .title').toggle()
- $('.header-content .header-logo').toggle()
- $('.header-content .navbar-collapse').toggle()
- $('.navbar-toggle').toggleClass('active')
-
- # Show/hide comments on diff
- $body.on "click", ".js-toggle-diff-comments", (e) ->
- $(@).toggleClass('active')
- $(@).closest(".diff-file").find(".notes_holder").toggle()
- e.preventDefault()
-
- $document.off "click", '.js-confirm-danger'
- $document.on "click", '.js-confirm-danger', (e) ->
- e.preventDefault()
- btn = $(e.target)
- text = btn.data("confirm-danger-message")
- form = btn.closest("form")
- new ConfirmDangerModal(form, text)
-
-
- $document.on 'click', 'button', ->
- $(this).blur()
-
- $('input[type="search"]').each ->
- $this = $(this)
- $this.attr 'value', $this.val()
- return
-
- $document
- .off 'keyup', 'input[type="search"]'
- .on 'keyup', 'input[type="search"]' , (e) ->
- $this = $(this)
- $this.attr 'value', $this.val()
-
- $sidebarGutterToggle = $('.js-sidebar-toggle')
-
- $document
- .off 'breakpoint:change'
- .on 'breakpoint:change', (e, breakpoint) ->
- if breakpoint is 'sm' or breakpoint is 'xs'
- $gutterIcon = $sidebarGutterToggle.find('i')
- if $gutterIcon.hasClass('fa-angle-double-right')
- $sidebarGutterToggle.trigger('click')
-
- fitSidebarForSize = ->
- oldBootstrapBreakpoint = bootstrapBreakpoint
- bootstrapBreakpoint = bp.getBreakpointSize()
- if bootstrapBreakpoint != oldBootstrapBreakpoint
- $document.trigger('breakpoint:change', [bootstrapBreakpoint])
-
- checkInitialSidebarSize = ->
- bootstrapBreakpoint = bp.getBreakpointSize()
- if bootstrapBreakpoint is "xs" or "sm"
- $document.trigger('breakpoint:change', [bootstrapBreakpoint])
-
- $window
- .off "resize.app"
- .on "resize.app", (e) ->
- fitSidebarForSize()
-
- gl.awardsHandler = new AwardsHandler()
- checkInitialSidebarSize()
- new Aside()
-
- # Sidenav pinning
- if $window.width() < 1024 and $.cookie('pin_nav') is 'true'
- $.cookie('pin_nav', 'false', { path: '/', expires: 365 * 10 })
- $('.page-with-sidebar')
- .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
- .removeClass('page-sidebar-pinned')
- $('.navbar-fixed-top').removeClass('header-pinned-nav')
-
- $document
- .off 'click', '.js-nav-pin'
- .on 'click', '.js-nav-pin', (e) ->
- e.preventDefault()
-
- $pinBtn = $(e.currentTarget)
- $page = $ '.page-with-sidebar'
- $topNav = $ '.navbar-fixed-top'
- $tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
- doPinNav = not $page.is('.page-sidebar-pinned')
- tooltipText = 'Pin navigation'
-
- $(this).toggleClass 'is-active'
-
- if doPinNav
- $page.addClass('page-sidebar-pinned')
- $topNav.addClass('header-pinned-nav')
- else
- $tooltip.remove() # Remove it immediately when collapsing the sidebar
- $page.removeClass('page-sidebar-pinned')
- .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
- $topNav.removeClass('header-pinned-nav')
- .toggleClass('header-collapsed header-expanded')
-
- # Save settings
- $.cookie 'pin_nav', doPinNav, { path: '/', expires: 365 * 10 }
-
- if $.cookie('pin_nav') is 'true' or doPinNav
- tooltipText = 'Unpin navigation'
-
- # Update tooltip text immediately
- $tooltip.find('.tooltip-inner').text(tooltipText)
-
- # Persist tooltip title
- $pinBtn.attr('title', tooltipText).tooltip('fixTitle')
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
new file mode 100644
index 00000000000..7b546e79ee0
--- /dev/null
+++ b/app/assets/javascripts/aside.js
@@ -0,0 +1,26 @@
+(function() {
+ this.Aside = (function() {
+ function Aside() {
+ $(document).off("click", "a.show-aside");
+ $(document).on("click", 'a.show-aside', function(e) {
+ var btn, icon;
+ e.preventDefault();
+ btn = $(e.currentTarget);
+ icon = btn.find('i');
+ if (icon.hasClass('fa-angle-left')) {
+ btn.parent().find('section').hide();
+ btn.parent().find('aside').fadeIn();
+ return icon.removeClass('fa-angle-left').addClass('fa-angle-right');
+ } else {
+ btn.parent().find('aside').hide();
+ btn.parent().find('section').fadeIn();
+ return icon.removeClass('fa-angle-right').addClass('fa-angle-left');
+ }
+ });
+ }
+
+ return Aside;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/aside.js.coffee b/app/assets/javascripts/aside.js.coffee
deleted file mode 100644
index 66ab5054326..00000000000
--- a/app/assets/javascripts/aside.js.coffee
+++ /dev/null
@@ -1,16 +0,0 @@
-class @Aside
- constructor: ->
- $(document).off "click", "a.show-aside"
- $(document).on "click", 'a.show-aside', (e) ->
- e.preventDefault()
- btn = $(e.currentTarget)
- icon = btn.find('i')
-
- if icon.hasClass('fa-angle-left')
- btn.parent().find('section').hide()
- btn.parent().find('aside').fadeIn()
- icon.removeClass('fa-angle-left').addClass('fa-angle-right')
- else
- btn.parent().find('aside').hide()
- btn.parent().find('section').fadeIn()
- icon.removeClass('fa-angle-right').addClass('fa-angle-left')
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
new file mode 100644
index 00000000000..7116512d6b7
--- /dev/null
+++ b/app/assets/javascripts/autosave.js
@@ -0,0 +1,63 @@
+(function() {
+ this.Autosave = (function() {
+ function Autosave(field, key) {
+ this.field = field;
+ if (key.join != null) {
+ key = key.join("/");
+ }
+ this.key = "autosave/" + key;
+ this.field.data("autosave", this);
+ this.restore();
+ this.field.on("input", (function(_this) {
+ return function() {
+ return _this.save();
+ };
+ })(this));
+ }
+
+ Autosave.prototype.restore = function() {
+ var e, error, text;
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ text = window.localStorage.getItem(this.key);
+ } catch (error) {
+ e = error;
+ return;
+ }
+ if ((text != null ? text.length : void 0) > 0) {
+ this.field.val(text);
+ }
+ return this.field.trigger("input");
+ };
+
+ Autosave.prototype.save = function() {
+ var text;
+ if (window.localStorage == null) {
+ return;
+ }
+ text = this.field.val();
+ if ((text != null ? text.length : void 0) > 0) {
+ try {
+ return window.localStorage.setItem(this.key, text);
+ } catch (undefined) {}
+ } else {
+ return this.reset();
+ }
+ };
+
+ Autosave.prototype.reset = function() {
+ if (window.localStorage == null) {
+ return;
+ }
+ try {
+ return window.localStorage.removeItem(this.key);
+ } catch (undefined) {}
+ };
+
+ return Autosave;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/autosave.js.coffee b/app/assets/javascripts/autosave.js.coffee
deleted file mode 100644
index 28f8e103664..00000000000
--- a/app/assets/javascripts/autosave.js.coffee
+++ /dev/null
@@ -1,39 +0,0 @@
-class @Autosave
- constructor: (field, key) ->
- @field = field
-
- key = key.join("/") if key.join?
- @key = "autosave/#{key}"
-
- @field.data "autosave", this
-
- @restore()
-
- @field.on "input", => @save()
-
- restore: ->
- return unless window.localStorage?
-
- try
- text = window.localStorage.getItem @key
- catch e
- return
-
- @field.val text if text?.length > 0
- @field.trigger "input"
-
- save: ->
- return unless window.localStorage?
-
- text = @field.val()
- if text?.length > 0
- try
- window.localStorage.setItem @key, text
- else
- @reset()
-
- reset: ->
- return unless window.localStorage?
-
- try
- window.localStorage.removeItem @key
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
deleted file mode 100644
index 37d0adaa625..00000000000
--- a/app/assets/javascripts/awards_handler.coffee
+++ /dev/null
@@ -1,372 +0,0 @@
-class @AwardsHandler
-
- constructor: ->
-
- @aliases = gl.emojiAliases()
-
- $(document)
- .off 'click', '.js-add-award'
- .on 'click', '.js-add-award', (e) =>
- e.stopPropagation()
- e.preventDefault()
-
- @showEmojiMenu $(e.currentTarget)
-
- $('html').on 'click', (e) ->
- $target = $ e.target
-
- unless $target.closest('.emoji-menu-content').length
- $('.js-awards-block.current').removeClass 'current'
-
- unless $target.closest('.emoji-menu').length
- if $('.emoji-menu').is(':visible')
- $('.js-add-award.is-active').removeClass 'is-active'
- $('.emoji-menu').removeClass 'is-visible'
-
- $(document)
- .off 'click', '.js-emoji-btn'
- .on 'click', '.js-emoji-btn', (e) =>
- e.preventDefault()
-
- $target = $ e.currentTarget
- emoji = $target.find('.icon').data 'emoji'
-
- $target.closest('.js-awards-block').addClass 'current'
- @addAward @getVotesBlock(), @getAwardUrl(), emoji
-
-
- showEmojiMenu: ($addBtn) ->
-
- $menu = $ '.emoji-menu'
-
- if $addBtn.hasClass 'js-note-emoji'
- $addBtn.closest('.note').find('.js-awards-block').addClass 'current'
- else
- $addBtn.closest('.js-awards-block').addClass 'current'
-
- if $menu.length
- $holder = $addBtn.closest('.js-award-holder')
-
- if $menu.is '.is-visible'
- $addBtn.removeClass 'is-active'
- $menu.removeClass 'is-visible'
- $('#emoji_search').blur()
- else
- $addBtn.addClass 'is-active'
- @positionMenu($menu, $addBtn)
-
- $menu.addClass 'is-visible'
- $('#emoji_search').focus()
- else
- $addBtn.addClass 'is-loading is-active'
- url = @getAwardMenuUrl()
-
- @createEmojiMenu url, =>
- $addBtn.removeClass 'is-loading'
- $menu = $('.emoji-menu')
- @positionMenu($menu, $addBtn)
- @renderFrequentlyUsedBlock() unless @frequentEmojiBlockRendered
-
- setTimeout =>
- $menu.addClass 'is-visible'
- $('#emoji_search').focus()
- @setupSearch()
- , 200
-
-
- createEmojiMenu: (awardMenuUrl, callback) ->
-
- $.get awardMenuUrl, (response) ->
- $('body').append response
- callback()
-
-
- positionMenu: ($menu, $addBtn) ->
-
- position = $addBtn.data('position')
-
- # The menu could potentially be off-screen or in a hidden overflow element
- # So we position the element absolute in the body
- css =
- top: "#{$addBtn.offset().top + $addBtn.outerHeight()}px"
-
- if position? and position is 'right'
- css.left = "#{($addBtn.offset().left - $menu.outerWidth()) + 20}px"
- $menu.addClass 'is-aligned-right'
- else
- css.left = "#{$addBtn.offset().left}px"
- $menu.removeClass 'is-aligned-right'
-
- $menu.css(css)
-
-
- addAward: (votesBlock, awardUrl, emoji, checkMutuality = true, callback) ->
-
- emoji = @normilizeEmojiName emoji
-
- @postEmoji awardUrl, emoji, =>
- @addAwardToEmojiBar votesBlock, emoji, checkMutuality
- callback?()
-
- $('.emoji-menu').removeClass 'is-visible'
-
-
- addAwardToEmojiBar: (votesBlock, emoji, checkForMutuality = true) ->
-
- @checkMutuality votesBlock, emoji if checkForMutuality
- @addEmojiToFrequentlyUsedList emoji
-
- emoji = @normilizeEmojiName emoji
- $emojiButton = @findEmojiIcon(votesBlock, emoji).parent()
-
- if $emojiButton.length > 0
- if @isActive $emojiButton
- @decrementCounter $emojiButton, emoji
- else
- counter = $emojiButton.find '.js-counter'
- counter.text parseInt(counter.text()) + 1
- $emojiButton.addClass 'active'
- @addMeToUserList votesBlock, emoji
- @animateEmoji $emojiButton
- else
- votesBlock.removeClass 'hidden'
- @createEmoji votesBlock, emoji
-
-
- getVotesBlock: ->
-
- currentBlock = $ '.js-awards-block.current'
- return if currentBlock.length then currentBlock else $('.js-awards-block').eq 0
-
-
- getAwardUrl: -> return @getVotesBlock().data 'award-url'
-
-
- checkMutuality: (votesBlock, emoji) ->
-
- awardUrl = @getAwardUrl()
-
- if emoji in [ 'thumbsup', 'thumbsdown' ]
- mutualVote = if emoji is 'thumbsup' then 'thumbsdown' else 'thumbsup'
- $emojiButton = votesBlock.find("[data-emoji=#{mutualVote}]").parent()
- isAlreadyVoted = $emojiButton.hasClass 'active'
-
- if isAlreadyVoted
- @showEmojiLoader $emojiButton
- @addAward votesBlock, awardUrl, mutualVote, false, ->
- $emojiButton.removeClass 'is-loading'
-
-
- showEmojiLoader: ($emojiButton) ->
-
- $loader = $emojiButton.find '.fa-spinner'
-
- unless $loader.length
- $emojiButton.append '<i class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading"></i>'
-
- $emojiButton.addClass 'is-loading'
-
-
- isActive: ($emojiButton) -> $emojiButton.hasClass 'active'
-
-
- decrementCounter: ($emojiButton, emoji) ->
-
- counter = $ '.js-counter', $emojiButton
- counterNumber = parseInt counter.text(), 10
-
- if counterNumber > 1
- counter.text counterNumber - 1
- @removeMeFromUserList $emojiButton, emoji
- else if emoji is 'thumbsup' or emoji is 'thumbsdown'
- $emojiButton.tooltip 'destroy'
- counter.text '0'
- @removeMeFromUserList $emojiButton, emoji
- @removeEmoji $emojiButton if $emojiButton.parents('.note').length
- else
- @removeEmoji $emojiButton
-
- $emojiButton.removeClass 'active'
-
-
- removeEmoji: ($emojiButton) ->
-
- $emojiButton.tooltip('destroy')
- $emojiButton.remove()
-
- $votesBlock = @getVotesBlock()
-
- if $votesBlock.find('.js-emoji-btn').length is 0
- $votesBlock.addClass 'hidden'
-
-
- getAwardTooltip: ($awardBlock) ->
-
- return $awardBlock.attr('data-original-title') or $awardBlock.attr('data-title') or ''
-
-
- removeMeFromUserList: ($emojiButton, emoji) ->
-
- awardBlock = $emojiButton
- originalTitle = @getAwardTooltip awardBlock
-
- authors = originalTitle.split ', '
- authors.splice authors.indexOf('me'), 1
-
- newAuthors = authors.join ', '
-
- awardBlock
- .closest '.js-emoji-btn'
- .removeData 'original-title'
- .attr 'data-original-title', newAuthors
-
- @resetTooltip awardBlock
-
-
- addMeToUserList: (votesBlock, emoji) ->
-
- awardBlock = @findEmojiIcon(votesBlock, emoji).parent()
- origTitle = @getAwardTooltip awardBlock
- users = []
-
- if origTitle
- users = origTitle.trim().split ', '
-
- users.push 'me'
- awardBlock.attr 'title', users.join ', '
-
- @resetTooltip awardBlock
-
-
- resetTooltip: (award) ->
-
- award.tooltip 'destroy'
-
- # 'destroy' call is asynchronous and there is no appropriate callback on it, this is why we need to set timeout.
- cb = -> award.tooltip()
- setTimeout cb, 200
-
-
- createEmoji_: (votesBlock, emoji) ->
-
- emojiCssClass = @resolveNameToCssClass emoji
- buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='me' data-placement='bottom'>
- <div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>
- <span class='award-control-text js-counter'>1</span>
- </button>"
-
- $emojiButton = $ buttonHtml
- $emojiButton
- .insertBefore votesBlock.find '.js-award-holder'
- .find '.emoji-icon'
- .data 'emoji', emoji
-
- @animateEmoji $emojiButton
- $('.award-control').tooltip()
- votesBlock.removeClass 'current'
-
-
- animateEmoji: ($emoji) ->
-
- className = 'pulse animated'
-
- $emoji.addClass className
- setTimeout (-> $emoji.removeClass className), 321
-
-
- createEmoji: (votesBlock, emoji) ->
-
- if $('.emoji-menu').length
- return @createEmoji_ votesBlock, emoji
-
- @createEmojiMenu @getAwardMenuUrl(), => @createEmoji_ votesBlock, emoji
-
-
- getAwardMenuUrl: -> return gon.award_menu_url
-
-
- resolveNameToCssClass: (emoji) ->
-
- emojiIcon = $ ".emoji-menu-content [data-emoji='#{emoji}']"
-
- if emojiIcon.length > 0
- unicodeName = emojiIcon.data 'unicode-name'
- else
- # Find by alias
- unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data 'unicode-name'
-
- return "emoji-#{unicodeName}"
-
-
- postEmoji: (awardUrl, emoji, callback) ->
-
- $.post awardUrl, { name: emoji }, (data) ->
- callback() if data.ok
-
-
- findEmojiIcon: (votesBlock, emoji) ->
-
- return votesBlock.find ".js-emoji-btn [data-emoji='#{emoji}']"
-
-
- scrollToAwards: ->
-
- options = scrollTop: $('.awards').offset().top - 110
- $('body, html').animate options, 200
-
-
- normilizeEmojiName: (emoji) -> return @aliases[emoji] or emoji
-
-
- addEmojiToFrequentlyUsedList: (emoji) ->
-
- frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
- frequentlyUsedEmojis.push emoji
- $.cookie 'frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 }
-
-
- getFrequentlyUsedEmojis: ->
-
- frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') or '').split(',')
- return _.compact _.uniq frequentlyUsedEmojis
-
-
- renderFrequentlyUsedBlock: ->
-
- if $.cookie 'frequently_used_emojis'
- frequentlyUsedEmojis = @getFrequentlyUsedEmojis()
-
- ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>")
-
- for emoji in frequentlyUsedEmojis
- $(".emoji-menu-content [data-emoji='#{emoji}']").closest('li').clone().appendTo(ul)
-
- $('.emoji-menu-content')
- .prepend(ul)
- .prepend($('<h5>').text('Frequently used'))
-
- @frequentEmojiBlockRendered = true
-
-
- setupSearch: ->
-
- $('input.emoji-search').on 'keyup', (ev) =>
- term = $(ev.target).val()
-
- # Clean previous search results
- $('ul.emoji-menu-search, h5.emoji-search').remove()
-
- if term
- # Generate a search result block
- h5 = $('<h5>').text('Search results')
- found_emojis = @searchEmojis(term).show()
- ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis)
- $('.emoji-menu-content ul, .emoji-menu-content h5').hide()
- $('.emoji-menu-content').append(h5).append(ul)
- else
- $('.emoji-menu-content').children().show()
-
-
- searchEmojis: (term) ->
-
- $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='#{term}']").closest('li').clone()
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
new file mode 100644
index 00000000000..ad12cb906e1
--- /dev/null
+++ b/app/assets/javascripts/awards_handler.js
@@ -0,0 +1,375 @@
+(function() {
+ this.AwardsHandler = (function() {
+ const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
+ function AwardsHandler() {
+ this.aliases = gl.emojiAliases();
+ $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) {
+ return function(e) {
+ e.stopPropagation();
+ e.preventDefault();
+ return _this.showEmojiMenu($(e.currentTarget));
+ };
+ })(this));
+ $('html').on('click', function(e) {
+ var $target;
+ $target = $(e.target);
+ if (!$target.closest('.emoji-menu-content').length) {
+ $('.js-awards-block.current').removeClass('current');
+ }
+ if (!$target.closest('.emoji-menu').length) {
+ if ($('.emoji-menu').is(':visible')) {
+ $('.js-add-award.is-active').removeClass('is-active');
+ return $('.emoji-menu').removeClass('is-visible');
+ }
+ }
+ });
+ $(document).off('click', '.js-emoji-btn').on('click', '.js-emoji-btn', (function(_this) {
+ return function(e) {
+ var $target, emoji;
+ e.preventDefault();
+ $target = $(e.currentTarget);
+ emoji = $target.find('.icon').data('emoji');
+ $target.closest('.js-awards-block').addClass('current');
+ return _this.addAward(_this.getVotesBlock(), _this.getAwardUrl(), emoji);
+ };
+ })(this));
+ }
+
+ AwardsHandler.prototype.showEmojiMenu = function($addBtn) {
+ var $holder, $menu, url;
+ $menu = $('.emoji-menu');
+ if ($addBtn.hasClass('js-note-emoji')) {
+ $addBtn.closest('.note').find('.js-awards-block').addClass('current');
+ } else {
+ $addBtn.closest('.js-awards-block').addClass('current');
+ }
+ if ($menu.length) {
+ $holder = $addBtn.closest('.js-award-holder');
+ if ($menu.is('.is-visible')) {
+ $addBtn.removeClass('is-active');
+ $menu.removeClass('is-visible');
+ return $('#emoji_search').blur();
+ } else {
+ $addBtn.addClass('is-active');
+ this.positionMenu($menu, $addBtn);
+ $menu.addClass('is-visible');
+ return $('#emoji_search').focus();
+ }
+ } else {
+ $addBtn.addClass('is-loading is-active');
+ url = this.getAwardMenuUrl();
+ return this.createEmojiMenu(url, (function(_this) {
+ return function() {
+ $addBtn.removeClass('is-loading');
+ $menu = $('.emoji-menu');
+ _this.positionMenu($menu, $addBtn);
+ if (!_this.frequentEmojiBlockRendered) {
+ _this.renderFrequentlyUsedBlock();
+ }
+ return setTimeout(function() {
+ $menu.addClass('is-visible');
+ $('#emoji_search').focus();
+ return _this.setupSearch();
+ }, 200);
+ };
+ })(this));
+ }
+ };
+
+ AwardsHandler.prototype.createEmojiMenu = function(awardMenuUrl, callback) {
+ return $.get(awardMenuUrl, function(response) {
+ $('body').append(response);
+ return callback();
+ });
+ };
+
+ AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
+ var css, position;
+ position = $addBtn.data('position');
+ css = {
+ top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
+ };
+ if ((position != null) && position === 'right') {
+ css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
+ $menu.addClass('is-aligned-right');
+ } else {
+ css.left = ($addBtn.offset().left) + "px";
+ $menu.removeClass('is-aligned-right');
+ }
+ return $menu.css(css);
+ };
+
+ AwardsHandler.prototype.addAward = function(votesBlock, awardUrl, emoji, checkMutuality, callback) {
+ if (checkMutuality == null) {
+ checkMutuality = true;
+ }
+ emoji = this.normilizeEmojiName(emoji);
+ this.postEmoji(awardUrl, emoji, (function(_this) {
+ return function() {
+ _this.addAwardToEmojiBar(votesBlock, emoji, checkMutuality);
+ return typeof callback === "function" ? callback() : void 0;
+ };
+ })(this));
+ return $('.emoji-menu').removeClass('is-visible');
+ };
+
+ AwardsHandler.prototype.addAwardToEmojiBar = function(votesBlock, emoji, checkForMutuality) {
+ var $emojiButton, counter;
+ if (checkForMutuality == null) {
+ checkForMutuality = true;
+ }
+ if (checkForMutuality) {
+ this.checkMutuality(votesBlock, emoji);
+ }
+ this.addEmojiToFrequentlyUsedList(emoji);
+ emoji = this.normilizeEmojiName(emoji);
+ $emojiButton = this.findEmojiIcon(votesBlock, emoji).parent();
+ if ($emojiButton.length > 0) {
+ if (this.isActive($emojiButton)) {
+ return this.decrementCounter($emojiButton, emoji);
+ } else {
+ counter = $emojiButton.find('.js-counter');
+ counter.text(parseInt(counter.text()) + 1);
+ $emojiButton.addClass('active');
+ this.addYouToUserList(votesBlock, emoji);
+ return this.animateEmoji($emojiButton);
+ }
+ } else {
+ votesBlock.removeClass('hidden');
+ return this.createEmoji(votesBlock, emoji);
+ }
+ };
+
+ AwardsHandler.prototype.getVotesBlock = function() {
+ var currentBlock;
+ currentBlock = $('.js-awards-block.current');
+ if (currentBlock.length) {
+ return currentBlock;
+ } else {
+ return $('.js-awards-block').eq(0);
+ }
+ };
+
+ AwardsHandler.prototype.getAwardUrl = function() {
+ return this.getVotesBlock().data('award-url');
+ };
+
+ AwardsHandler.prototype.checkMutuality = function(votesBlock, emoji) {
+ var $emojiButton, awardUrl, isAlreadyVoted, mutualVote;
+ awardUrl = this.getAwardUrl();
+ if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+ mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
+ $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent();
+ isAlreadyVoted = $emojiButton.hasClass('active');
+ if (isAlreadyVoted) {
+ this.addAward(votesBlock, awardUrl, mutualVote, false);
+ }
+ }
+ };
+
+ AwardsHandler.prototype.isActive = function($emojiButton) {
+ return $emojiButton.hasClass('active');
+ };
+
+ AwardsHandler.prototype.decrementCounter = function($emojiButton, emoji) {
+ var counter, counterNumber;
+ counter = $('.js-counter', $emojiButton);
+ counterNumber = parseInt(counter.text(), 10);
+ if (counterNumber > 1) {
+ counter.text(counterNumber - 1);
+ this.removeYouFromUserList($emojiButton, emoji);
+ } else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
+ $emojiButton.tooltip('destroy');
+ counter.text('0');
+ this.removeYouFromUserList($emojiButton, emoji);
+ if ($emojiButton.parents('.note').length) {
+ this.removeEmoji($emojiButton);
+ }
+ } else {
+ this.removeEmoji($emojiButton);
+ }
+ return $emojiButton.removeClass('active');
+ };
+
+ AwardsHandler.prototype.removeEmoji = function($emojiButton) {
+ var $votesBlock;
+ $emojiButton.tooltip('destroy');
+ $emojiButton.remove();
+ $votesBlock = this.getVotesBlock();
+ if ($votesBlock.find('.js-emoji-btn').length === 0) {
+ return $votesBlock.addClass('hidden');
+ }
+ };
+
+ AwardsHandler.prototype.getAwardTooltip = function($awardBlock) {
+ return $awardBlock.attr('data-original-title') || $awardBlock.attr('data-title') || '';
+ };
+
+ AwardsHandler.prototype.toSentence = function(list) {
+ if(list.length <= 2){
+ return list.join(' and ');
+ }
+ else{
+ return list.slice(0, -1).join(', ') + ', and ' + list[list.length - 1];
+ }
+ };
+
+ AwardsHandler.prototype.removeYouFromUserList = function($emojiButton, emoji) {
+ var authors, awardBlock, newAuthors, originalTitle;
+ awardBlock = $emojiButton;
+ originalTitle = this.getAwardTooltip(awardBlock);
+ authors = originalTitle.split(FROM_SENTENCE_REGEX);
+ authors.splice(authors.indexOf('You'), 1);
+ return awardBlock
+ .closest('.js-emoji-btn')
+ .removeData('title')
+ .removeAttr('data-title')
+ .removeAttr('data-original-title')
+ .attr('title', this.toSentence(authors))
+ .tooltip('fixTitle');
+ };
+
+ AwardsHandler.prototype.addYouToUserList = function(votesBlock, emoji) {
+ var awardBlock, origTitle, users;
+ awardBlock = this.findEmojiIcon(votesBlock, emoji).parent();
+ origTitle = this.getAwardTooltip(awardBlock);
+ users = [];
+ if (origTitle) {
+ users = origTitle.trim().split(FROM_SENTENCE_REGEX);
+ }
+ users.unshift('You');
+ return awardBlock
+ .attr('title', this.toSentence(users))
+ .tooltip('fixTitle');
+ };
+
+ AwardsHandler.prototype.createEmoji_ = function(votesBlock, emoji) {
+ var $emojiButton, buttonHtml, emojiCssClass;
+ emojiCssClass = this.resolveNameToCssClass(emoji);
+ buttonHtml = "<button class='btn award-control js-emoji-btn has-tooltip active' title='You' data-placement='bottom'> <div class='icon emoji-icon " + emojiCssClass + "' data-emoji='" + emoji + "'></div> <span class='award-control-text js-counter'>1</span> </button>";
+ $emojiButton = $(buttonHtml);
+ $emojiButton.insertBefore(votesBlock.find('.js-award-holder')).find('.emoji-icon').data('emoji', emoji);
+ this.animateEmoji($emojiButton);
+ $('.award-control').tooltip();
+ return votesBlock.removeClass('current');
+ };
+
+ AwardsHandler.prototype.animateEmoji = function($emoji) {
+ var className;
+ className = 'pulse animated';
+ $emoji.addClass(className);
+ return setTimeout((function() {
+ return $emoji.removeClass(className);
+ }), 321);
+ };
+
+ AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
+ if ($('.emoji-menu').length) {
+ return this.createEmoji_(votesBlock, emoji);
+ }
+ return this.createEmojiMenu(this.getAwardMenuUrl(), (function(_this) {
+ return function() {
+ return _this.createEmoji_(votesBlock, emoji);
+ };
+ })(this));
+ };
+
+ AwardsHandler.prototype.getAwardMenuUrl = function() {
+ return gon.award_menu_url;
+ };
+
+ AwardsHandler.prototype.resolveNameToCssClass = function(emoji) {
+ var emojiIcon, unicodeName;
+ emojiIcon = $(".emoji-menu-content [data-emoji='" + emoji + "']");
+ if (emojiIcon.length > 0) {
+ unicodeName = emojiIcon.data('unicode-name');
+ } else {
+ unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
+ }
+ return "emoji-" + unicodeName;
+ };
+
+ AwardsHandler.prototype.postEmoji = function(awardUrl, emoji, callback) {
+ return $.post(awardUrl, {
+ name: emoji
+ }, function(data) {
+ if (data.ok) {
+ return callback();
+ }
+ });
+ };
+
+ AwardsHandler.prototype.findEmojiIcon = function(votesBlock, emoji) {
+ return votesBlock.find(".js-emoji-btn [data-emoji='" + emoji + "']");
+ };
+
+ AwardsHandler.prototype.scrollToAwards = function() {
+ var options;
+ options = {
+ scrollTop: $('.awards').offset().top - 110
+ };
+ return $('body, html').animate(options, 200);
+ };
+
+ AwardsHandler.prototype.normilizeEmojiName = function(emoji) {
+ return this.aliases[emoji] || emoji;
+ };
+
+ AwardsHandler.prototype.addEmojiToFrequentlyUsedList = function(emoji) {
+ var frequentlyUsedEmojis;
+ frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
+ frequentlyUsedEmojis.push(emoji);
+ return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
+ path: gon.relative_url_root || '/',
+ expires: 365
+ });
+ };
+
+ AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
+ var frequentlyUsedEmojis;
+ frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',');
+ return _.compact(_.uniq(frequentlyUsedEmojis));
+ };
+
+ AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
+ var emoji, frequentlyUsedEmojis, i, len, ul;
+ if ($.cookie('frequently_used_emojis')) {
+ frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
+ ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
+ for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
+ emoji = frequentlyUsedEmojis[i];
+ $(".emoji-menu-content [data-emoji='" + emoji + "']").closest('li').clone().appendTo(ul);
+ }
+ $('.emoji-menu-content').prepend(ul).prepend($('<h5>').text('Frequently used'));
+ }
+ return this.frequentEmojiBlockRendered = true;
+ };
+
+ AwardsHandler.prototype.setupSearch = function() {
+ return $('input.emoji-search').on('keyup', (function(_this) {
+ return function(ev) {
+ var found_emojis, h5, term, ul;
+ term = $(ev.target).val();
+ $('ul.emoji-menu-search, h5.emoji-search').remove();
+ if (term) {
+ h5 = $('<h5>').text('Search results');
+ found_emojis = _this.searchEmojis(term).show();
+ ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
+ $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
+ return $('.emoji-menu-content').append(h5).append(ul);
+ } else {
+ return $('.emoji-menu-content').children().show();
+ }
+ };
+ })(this));
+ };
+
+ AwardsHandler.prototype.searchEmojis = function(term) {
+ return $(".emoji-menu-list:not(.frequent-emojis) [data-emoji*='" + term + "']").closest('li').clone();
+ };
+
+ return AwardsHandler;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
new file mode 100644
index 00000000000..f977a1e8a7b
--- /dev/null
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -0,0 +1,30 @@
+
+/*= require jquery.ba-resize */
+
+
+/*= require autosize */
+
+(function() {
+ $(function() {
+ var $fields;
+ $fields = $('.js-autosize');
+ $fields.on('autosize:resized', function() {
+ var $field;
+ $field = $(this);
+ return $field.data('height', $field.outerHeight());
+ });
+ $fields.on('resize.autosize', function() {
+ var $field;
+ $field = $(this);
+ if ($field.data('height') !== $field.outerHeight()) {
+ $field.data('height', $field.outerHeight());
+ autosize.destroy($field);
+ return $field.css('max-height', window.outerHeight);
+ }
+ });
+ autosize($fields);
+ autosize.update($fields);
+ return $fields.css('resize', 'vertical');
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee
deleted file mode 100644
index a072fe48a98..00000000000
--- a/app/assets/javascripts/behaviors/autosize.js.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-#= require jquery.ba-resize
-#= require autosize
-
-$ ->
- $fields = $('.js-autosize')
-
- $fields.on 'autosize:resized', ->
- $field = $(@)
- $field.data('height', $field.outerHeight())
-
- $fields.on 'resize.autosize', ->
- $field = $(@)
-
- if $field.data('height') != $field.outerHeight()
- $field.data('height', $field.outerHeight())
- autosize.destroy($field)
- $field.css('max-height', window.outerHeight)
-
- autosize($fields)
- autosize.update($fields)
-
- $fields.css('resize', 'vertical')
diff --git a/app/assets/javascripts/behaviors/details_behavior.coffee b/app/assets/javascripts/behaviors/details_behavior.coffee
deleted file mode 100644
index decab3e1bed..00000000000
--- a/app/assets/javascripts/behaviors/details_behavior.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-$ ->
- $("body").on "click", ".js-details-target", ->
- container = $(@).closest(".js-details-container")
- container.toggleClass("open")
-
- # Show details content. Hides link after click.
- #
- # %div
- # %a.js-details-expand
- # %div.js-details-content
- #
- $("body").on "click", ".js-details-expand", (e) ->
- $(@).next('.js-details-content').removeClass("hide")
- $(@).hide()
- e.preventDefault()
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
new file mode 100644
index 00000000000..3631d1b74ac
--- /dev/null
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -0,0 +1,15 @@
+(function() {
+ $(function() {
+ $("body").on("click", ".js-details-target", function() {
+ var container;
+ container = $(this).closest(".js-details-container");
+ return container.toggleClass("open");
+ });
+ return $("body").on("click", ".js-details-expand", function(e) {
+ $(this).next('.js-details-content').removeClass("hide");
+ $(this).hide();
+ return e.preventDefault();
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
new file mode 100644
index 00000000000..3527d0a95fc
--- /dev/null
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -0,0 +1,58 @@
+
+/*= require extensions/jquery */
+
+(function() {
+ var isMac, keyCodeIs;
+
+ isMac = function() {
+ return navigator.userAgent.match(/Macintosh/);
+ };
+
+ keyCodeIs = function(e, keyCode) {
+ if ((e.originalEvent && e.originalEvent.repeat) || e.repeat) {
+ return false;
+ }
+ return e.keyCode === keyCode;
+ };
+
+ $(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
+ var $form, $submit_button;
+ if (!keyCodeIs(e, 13)) {
+ return;
+ }
+ if (!((e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey))) {
+ return;
+ }
+ e.preventDefault();
+ $form = $(e.target).closest('form');
+ $submit_button = $form.find('input[type=submit], button[type=submit]');
+ if ($submit_button.attr('disabled')) {
+ return;
+ }
+ $submit_button.disable();
+ return $form.submit();
+ });
+
+ $(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
+ var $this, title;
+ if (!keyCodeIs(e, 9)) {
+ return;
+ }
+ if (isMac()) {
+ title = "You can also press &#8984;-Enter";
+ } else {
+ title = "You can also press Ctrl-Enter";
+ }
+ $this = $(this);
+ return $this.tooltip({
+ container: 'body',
+ html: 'true',
+ placement: 'auto top',
+ title: title,
+ trigger: 'manual'
+ }).tooltip('show').one('blur', function() {
+ return $this.tooltip('hide');
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/behaviors/quick_submit.js.coffee b/app/assets/javascripts/behaviors/quick_submit.js.coffee
deleted file mode 100644
index 3cb96bacaa7..00000000000
--- a/app/assets/javascripts/behaviors/quick_submit.js.coffee
+++ /dev/null
@@ -1,56 +0,0 @@
-# Quick Submit behavior
-#
-# When a child field of a form with a `js-quick-submit` class receives a
-# "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
-# is submitted.
-#
-#= require extensions/jquery
-#
-# ### Example Markup
-#
-# <form action="/foo" class="js-quick-submit">
-# <input type="text" />
-# <textarea></textarea>
-# <input type="submit" value="Submit" />
-# </form>
-#
-isMac = ->
- navigator.userAgent.match(/Macintosh/)
-
-keyCodeIs = (e, keyCode) ->
- return false if (e.originalEvent && e.originalEvent.repeat) || e.repeat
- return e.keyCode == keyCode
-
-$(document).on 'keydown.quick_submit', '.js-quick-submit', (e) ->
- return unless keyCodeIs(e, 13) # Enter
-
- return unless (e.metaKey && !e.altKey && !e.ctrlKey && !e.shiftKey) || (e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey)
-
- e.preventDefault()
-
- $form = $(e.target).closest('form')
- $submit_button = $form.find('input[type=submit], button[type=submit]')
-
- return if $submit_button.attr('disabled')
-
- $submit_button.disable()
- $form.submit()
-
-# If the user tabs to a submit button on a `js-quick-submit` form, display a
-# tooltip to let them know they could've used the hotkey
-$(document).on 'keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', (e) ->
- return unless keyCodeIs(e, 9) # Tab
-
- if isMac()
- title = "You can also press &#8984;-Enter"
- else
- title = "You can also press Ctrl-Enter"
-
- $this = $(@)
- $this.tooltip(
- container: 'body'
- html: 'true'
- placement: 'auto top'
- title: title
- trigger: 'manual'
- ).tooltip('show').one('blur', -> $this.tooltip('hide'))
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
new file mode 100644
index 00000000000..db0b36b24e9
--- /dev/null
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -0,0 +1,45 @@
+
+/*= require extensions/jquery */
+
+(function() {
+ $.fn.requiresInput = function() {
+ var $button, $form, fieldSelector, requireInput, required;
+ $form = $(this);
+ $button = $('button[type=submit], input[type=submit]', $form);
+ required = '[required=required]';
+ fieldSelector = "input" + required + ", select" + required + ", textarea" + required;
+ requireInput = function() {
+ var values;
+ values = _.map($(fieldSelector, $form), function(field) {
+ return field.value;
+ });
+ if (values.length && _.any(values, _.isEmpty)) {
+ return $button.disable();
+ } else {
+ return $button.enable();
+ }
+ };
+ requireInput();
+ return $form.on('change input', fieldSelector, requireInput);
+ };
+
+ $(function() {
+ var $form, hideOrShowHelpBlock;
+ $form = $('form.js-requires-input');
+ $form.requiresInput();
+ hideOrShowHelpBlock = function(form) {
+ var selected;
+ selected = $('.js-select-namespace option:selected');
+ if (selected.length && selected.data('options-parent') === 'groups') {
+ return form.find('.help-block').hide();
+ } else if (selected.length) {
+ return form.find('.help-block').show();
+ }
+ };
+ hideOrShowHelpBlock($form);
+ return $('.select2.js-select-namespace').change(function() {
+ return hideOrShowHelpBlock($form);
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/behaviors/requires_input.js.coffee b/app/assets/javascripts/behaviors/requires_input.js.coffee
deleted file mode 100644
index 0faa570ce13..00000000000
--- a/app/assets/javascripts/behaviors/requires_input.js.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-# Requires Input behavior
-#
-# When called on a form with input fields with the `required` attribute, the
-# form's submit button will be disabled until all required fields have values.
-#
-#= require extensions/jquery
-#
-# ### Example Markup
-#
-# <form class="js-requires-input">
-# <input type="text" required="required">
-# <input type="submit" value="Submit">
-# </form>
-#
-$.fn.requiresInput = ->
- $form = $(this)
- $button = $('button[type=submit], input[type=submit]', $form)
-
- required = '[required=required]'
- fieldSelector = "input#{required}, select#{required}, textarea#{required}"
-
- requireInput = ->
- # Collect the input values of *all* required fields
- values = _.map $(fieldSelector, $form), (field) -> field.value
-
- # Disable the button if any required fields are empty
- if values.length && _.any(values, _.isEmpty)
- $button.disable()
- else
- $button.enable()
-
- # Set initial button state
- requireInput()
-
- $form.on 'change input', fieldSelector, requireInput
-
-$ ->
- $form = $('form.js-requires-input')
- $form.requiresInput()
-
- # Hide or Show the help block when creating a new project
- # based on the option selected
- hideOrShowHelpBlock = (form) ->
- selected = $('.js-select-namespace option:selected')
- if selected.length and selected.data('options-parent') is 'groups'
- return form.find('.help-block').hide()
- else if selected.length
- form.find('.help-block').show()
-
- hideOrShowHelpBlock($form)
-
- $('.select2.js-select-namespace').change -> hideOrShowHelpBlock($form)
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.coffee b/app/assets/javascripts/behaviors/toggler_behavior.coffee
deleted file mode 100644
index 177b6918270..00000000000
--- a/app/assets/javascripts/behaviors/toggler_behavior.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-$ ->
- # Toggle button. Show/hide content inside parent container.
- # Button does not change visibility. If button has icon - it changes chevron style.
- #
- # %div.js-toggle-container
- # %a.js-toggle-button
- # %div.js-toggle-content
- #
- $("body").on "click", ".js-toggle-button", (e) ->
- $(@).find('i').
- toggleClass('fa fa-chevron-down').
- toggleClass('fa fa-chevron-up')
- $(@).closest(".js-toggle-container").find(".js-toggle-content").toggle()
- e.preventDefault()
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
new file mode 100644
index 00000000000..8ac1ba7665e
--- /dev/null
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -0,0 +1,26 @@
+(function(w) {
+ $(function() {
+ $('.js-toggle-button').on('click', function(e) {
+ e.preventDefault();
+ $(this)
+ .find('.fa')
+ .toggleClass('fa-chevron-down fa-chevron-up')
+ .end()
+ .closest('.js-toggle-container')
+ .find('.js-toggle-content')
+ .toggle()
+ ;
+ });
+
+ // If we're accessing a permalink, ensure it is not inside a
+ // closed js-toggle-container!
+ var hash = w.gl.utils.getLocationHash();
+ var anchor = hash && document.getElementById(hash);
+ var container = anchor && $(anchor).closest('.js-toggle-container');
+
+ if (container && container.find('.js-toggle-content').is(':hidden')) {
+ container.find('.js-toggle-button').trigger('click');
+ anchor.scrollIntoView();
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
new file mode 100644
index 00000000000..68758574967
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js
@@ -0,0 +1,46 @@
+
+/*= require blob/template_selector */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.BlobCiYamlSelector = (function(superClass) {
+ extend(BlobCiYamlSelector, superClass);
+
+ function BlobCiYamlSelector() {
+ return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
+ }
+
+ BlobCiYamlSelector.prototype.requestFile = function(query) {
+ return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
+ };
+
+ return BlobCiYamlSelector;
+
+ })(TemplateSelector);
+
+ this.BlobCiYamlSelectors = (function() {
+ function BlobCiYamlSelectors(opts) {
+ var ref;
+ this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
+ this.$dropdowns.each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ editor: _this.editor
+ });
+ };
+ })(this));
+ }
+
+ return BlobCiYamlSelectors;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
deleted file mode 100644
index d9a03d05529..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-#= require blob/template_selector
-
-class @BlobCiYamlSelector extends TemplateSelector
- requestFile: (query) ->
- Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
-
-class @BlobCiYamlSelectors
- constructor: (opts) ->
- {
- @$dropdowns = $('.js-gitlab-ci-yml-selector')
- @editor
- } = opts
-
- @$dropdowns.each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- new BlobCiYamlSelector(
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown,
- editor: @editor
- )
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
new file mode 100644
index 00000000000..f4044f22db2
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -0,0 +1,62 @@
+(function() {
+ this.BlobFileDropzone = (function() {
+ function BlobFileDropzone(form, method) {
+ var dropzone, form_dropzone, submitButton;
+ form_dropzone = form.find('.dropzone');
+ Dropzone.autoDiscover = false;
+ dropzone = form_dropzone.dropzone({
+ autoDiscover: false,
+ autoProcessQueue: false,
+ url: form.attr('action'),
+ method: method,
+ clickable: true,
+ uploadMultiple: false,
+ paramName: "file",
+ maxFilesize: gon.max_file_size || 10,
+ parallelUploads: 1,
+ maxFiles: 1,
+ addRemoveLinks: true,
+ previewsContainer: '.dropzone-previews',
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ init: function() {
+ this.on('addedfile', function(file) {
+ $('.dropzone-alerts').html('').hide();
+ });
+ this.on('success', function(header, response) {
+ window.location.href = response.filePath;
+ });
+ this.on('maxfilesexceeded', function(file) {
+ this.removeFile(file);
+ });
+ return this.on('sending', function(file, xhr, formData) {
+ formData.append('target_branch', form.find('.js-target-branch').val());
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val());
+ formData.append('commit_message', form.find('.js-commit-message').val());
+ });
+ },
+ error: function(file, errorMessage) {
+ var stripped;
+ stripped = $("<div/>").html(errorMessage).text();
+ $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show();
+ this.removeFile(file);
+ }
+ });
+ submitButton = form.find('#submit-all')[0];
+ submitButton.addEventListener('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
+ alert("Please select a file");
+ }
+ dropzone[0].dropzone.processQueue();
+ return false;
+ });
+ }
+
+ return BlobFileDropzone;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
deleted file mode 100644
index 9df932817f6..00000000000
--- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-class @BlobFileDropzone
- constructor: (form, method) ->
- form_dropzone = form.find('.dropzone')
- Dropzone.autoDiscover = false
- dropzone = form_dropzone.dropzone(
- autoDiscover: false
- autoProcessQueue: false
- url: form.attr('action')
- # Rails uses a hidden input field for PUT
- # http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
- method: method
- clickable: true
- uploadMultiple: false
- paramName: "file"
- maxFilesize: gon.max_file_size or 10
- parallelUploads: 1
- maxFiles: 1
- addRemoveLinks: true
- previewsContainer: '.dropzone-previews'
- headers:
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
-
- init: ->
- this.on 'addedfile', (file) ->
- $('.dropzone-alerts').html('').hide()
-
- return
-
- this.on 'success', (header, response) ->
- window.location.href = response.filePath
- return
-
- this.on 'maxfilesexceeded', (file) ->
- @removeFile file
- return
-
- this.on 'sending', (file, xhr, formData) ->
- formData.append('target_branch', form.find('.js-target-branch').val())
- formData.append('create_merge_request', form.find('.js-create-merge-request').val())
- formData.append('commit_message', form.find('.js-commit-message').val())
- return
-
- # Override behavior of adding error underneath preview
- error: (file, errorMessage) ->
- stripped = $("<div/>").html(errorMessage).text();
- $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show()
- @removeFile file
- return
- )
-
- submitButton = form.find('#submit-all')[0]
- submitButton.addEventListener 'click', (e) ->
- e.preventDefault()
- e.stopPropagation()
- alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0
- dropzone[0].dropzone.processQueue()
- return false
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
new file mode 100644
index 00000000000..54a09e919f8
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -0,0 +1,23 @@
+
+/*= require blob/template_selector */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.BlobGitignoreSelector = (function(superClass) {
+ extend(BlobGitignoreSelector, superClass);
+
+ function BlobGitignoreSelector() {
+ return BlobGitignoreSelector.__super__.constructor.apply(this, arguments);
+ }
+
+ BlobGitignoreSelector.prototype.requestFile = function(query) {
+ return Api.gitignoreText(query.name, this.requestFileSuccess.bind(this));
+ };
+
+ return BlobGitignoreSelector;
+
+ })(TemplateSelector);
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
deleted file mode 100644
index 8d0e3f363d1..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-#= require blob/template_selector
-
-class @BlobGitignoreSelector extends TemplateSelector
- requestFile: (query) ->
- Api.gitignoreText query.name, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
new file mode 100644
index 00000000000..4e9500428b2
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -0,0 +1,25 @@
+(function() {
+ this.BlobGitignoreSelectors = (function() {
+ function BlobGitignoreSelectors(opts) {
+ var ref;
+ this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitignore-selector'), this.editor = opts.editor;
+ this.$dropdowns.each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return new BlobGitignoreSelector({
+ pattern: /(.gitignore)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
+ dropdown: $dropdown,
+ editor: _this.editor
+ });
+ };
+ })(this));
+ }
+
+ return BlobGitignoreSelectors;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee b/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
deleted file mode 100644
index a719ba25122..00000000000
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-class @BlobGitignoreSelectors
- constructor: (opts) ->
- {
- @$dropdowns = $('.js-gitignore-selector')
- @editor
- } = opts
-
- @$dropdowns.each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- new BlobGitignoreSelector(
- pattern: /(.gitignore)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
- dropdown: $dropdown,
- editor: @editor
- )
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
new file mode 100644
index 00000000000..9a8ef08f4e5
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -0,0 +1,28 @@
+
+/*= require blob/template_selector */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.BlobLicenseSelector = (function(superClass) {
+ extend(BlobLicenseSelector, superClass);
+
+ function BlobLicenseSelector() {
+ return BlobLicenseSelector.__super__.constructor.apply(this, arguments);
+ }
+
+ BlobLicenseSelector.prototype.requestFile = function(query) {
+ var data;
+ data = {
+ project: this.dropdown.data('project'),
+ fullname: this.dropdown.data('fullname')
+ };
+ return Api.licenseText(query.id, data, this.requestFileSuccess.bind(this));
+ };
+
+ return BlobLicenseSelector;
+
+ })(TemplateSelector);
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
deleted file mode 100644
index a3cc8dd844c..00000000000
--- a/app/assets/javascripts/blob/blob_license_selector.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-#= require blob/template_selector
-
-class @BlobLicenseSelector extends TemplateSelector
- requestFile: (query) ->
- data =
- project: @dropdown.data('project')
- fullname: @dropdown.data('fullname')
-
- Api.licenseText query.id, data, @requestFileSuccess.bind(@)
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
new file mode 100644
index 00000000000..39237705e8d
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js
@@ -0,0 +1,25 @@
+(function() {
+ this.BlobLicenseSelectors = (function() {
+ function BlobLicenseSelectors(opts) {
+ var ref;
+ this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
+ this.$dropdowns.each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return new BlobLicenseSelector({
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ editor: _this.editor
+ });
+ };
+ })(this));
+ }
+
+ return BlobLicenseSelectors;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.coffee b/app/assets/javascripts/blob/blob_license_selectors.js.coffee
deleted file mode 100644
index 68438733108..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-class @BlobLicenseSelectors
- constructor: (opts) ->
- {
- @$dropdowns = $('.js-license-selector')
- @editor
- } = opts
-
- @$dropdowns.each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- new BlobLicenseSelector(
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- editor: @editor
- )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
deleted file mode 100644
index 19e584519d7..00000000000
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ /dev/null
@@ -1,42 +0,0 @@
-class @EditBlob
- constructor: (assets_path, ace_mode = null) ->
- ace.config.set "modePath", "#{assets_path}/ace"
- ace.config.loadModule "ace/ext/searchbox"
- @editor = ace.edit("editor")
- @editor.focus()
- @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode
-
- # Before a form submission, move the content from the Ace editor into the
- # submitted textarea
- $('form').submit =>
- $("#file-content").val(@editor.getValue())
-
- @initModePanesAndLinks()
-
- new BlobLicenseSelectors { @editor }
- new BlobGitignoreSelectors { @editor }
- new BlobCiYamlSelectors { @editor }
-
- initModePanesAndLinks: ->
- @$editModePanes = $(".js-edit-mode-pane")
- @$editModeLinks = $(".js-edit-mode a")
- @$editModeLinks.click @editModeLinkClickHandler
-
- editModeLinkClickHandler: (event) =>
- event.preventDefault()
- currentLink = $(event.target)
- paneId = currentLink.attr("href")
- currentPane = @$editModePanes.filter(paneId)
- @$editModeLinks.parent().removeClass "active hover"
- currentLink.parent().addClass "active hover"
- @$editModePanes.hide()
- currentPane.fadeIn 200
- if paneId is "#preview"
- $.post currentLink.data("preview-url"),
- content: @editor.getValue()
- , (response) ->
- currentPane.empty().append response
- currentPane.syntaxHighlight()
-
- else
- @editor.focus()
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
new file mode 100644
index 00000000000..b0a37ef0e0a
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -0,0 +1,90 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.TemplateSelector = (function() {
+ function TemplateSelector(opts) {
+ var ref;
+ if (opts == null) {
+ opts = {};
+ }
+ this.onClick = bind(this.onClick, this);
+ this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+ this.buildDropdown();
+ this.bindEvents();
+ this.onFilenameUpdate();
+ }
+
+ TemplateSelector.prototype.buildDropdown = function() {
+ return this.dropdown.glDropdown({
+ data: this.data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: this.toggleLabel,
+ search: {
+ fields: ['name']
+ },
+ clicked: this.onClick,
+ text: function(item) {
+ return item.name;
+ }
+ });
+ };
+
+ TemplateSelector.prototype.bindEvents = function() {
+ return this.$input.on('keyup blur', (function(_this) {
+ return function(e) {
+ return _this.onFilenameUpdate();
+ };
+ })(this));
+ };
+
+ TemplateSelector.prototype.toggleLabel = function(item) {
+ return item.name;
+ };
+
+ TemplateSelector.prototype.onFilenameUpdate = function() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
+ }
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
+ }
+ return this.wrapper.removeClass('hidden');
+ };
+
+ TemplateSelector.prototype.onClick = function(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ };
+
+ TemplateSelector.prototype.requestFile = function(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ };
+
+ TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
+ this.editor.setValue(file.content, 1);
+ if (!skipFocus) this.editor.focus();
+ };
+
+ TemplateSelector.prototype.startLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ };
+
+ TemplateSelector.prototype.stopLoadingSpinner = function() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ };
+
+ return TemplateSelector;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/template_selector.js.coffee b/app/assets/javascripts/blob/template_selector.js.coffee
deleted file mode 100644
index 40c9169beac..00000000000
--- a/app/assets/javascripts/blob/template_selector.js.coffee
+++ /dev/null
@@ -1,60 +0,0 @@
-class @TemplateSelector
- constructor: (opts = {}) ->
- {
- @dropdown,
- @data,
- @pattern,
- @wrapper,
- @editor,
- @fileEndpoint,
- @$input = $('#file_name')
- } = opts
-
- @buildDropdown()
- @bindEvents()
- @onFilenameUpdate()
-
- buildDropdown: ->
- @dropdown.glDropdown(
- data: @data,
- filterable: true,
- selectable: true,
- toggleLabel: @toggleLabel,
- search:
- fields: ['name']
- clicked: @onClick
- text: (item) ->
- item.name
- )
-
- bindEvents: ->
- @$input.on('keyup blur', (e) =>
- @onFilenameUpdate()
- )
-
- toggleLabel: (item) ->
- item.name
-
- onFilenameUpdate: ->
- return unless @$input.length
-
- filenameMatches = @pattern.test(@$input.val().trim())
-
- if not filenameMatches
- @wrapper.addClass('hidden')
- return
-
- @wrapper.removeClass('hidden')
-
- onClick: (item, el, e) =>
- e.preventDefault()
- @requestFile(item)
-
- requestFile: (item) ->
- # To be implemented on the extending class
- # e.g.
- # Api.gitignoreText item.name, @requestFileSuccess.bind(@)
-
- requestFileSuccess: (file) ->
- @editor.setValue(file.content, 1)
- @editor.focus()
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
new file mode 100644
index 00000000000..2afef43f3d6
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var url = $(".js-edit-blob-form").data("relative-url-root");
+ url += $(".js-edit-blob-form").data("assets-prefix");
+
+ var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
+ new NewCommitForm($('.js-edit-blob-form'));
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
new file mode 100644
index 00000000000..649c79daee8
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -0,0 +1,66 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.EditBlob = (function() {
+ function EditBlob(assets_path, ace_mode) {
+ if (ace_mode == null) {
+ ace_mode = null;
+ }
+ this.editModeLinkClickHandler = bind(this.editModeLinkClickHandler, this);
+ ace.config.set("modePath", assets_path + "/ace");
+ ace.config.loadModule("ace/ext/searchbox");
+ this.editor = ace.edit("editor");
+ this.editor.focus();
+ if (ace_mode) {
+ this.editor.getSession().setMode("ace/mode/" + ace_mode);
+ }
+ $('form').submit((function(_this) {
+ return function() {
+ return $("#file-content").val(_this.editor.getValue());
+ };
+ })(this));
+ this.initModePanesAndLinks();
+ new BlobLicenseSelectors({
+ editor: this.editor
+ });
+ new BlobGitignoreSelectors({
+ editor: this.editor
+ });
+ new BlobCiYamlSelectors({
+ editor: this.editor
+ });
+ }
+
+ EditBlob.prototype.initModePanesAndLinks = function() {
+ this.$editModePanes = $(".js-edit-mode-pane");
+ this.$editModeLinks = $(".js-edit-mode a");
+ return this.$editModeLinks.click(this.editModeLinkClickHandler);
+ };
+
+ EditBlob.prototype.editModeLinkClickHandler = function(event) {
+ var currentLink, currentPane, paneId;
+ event.preventDefault();
+ currentLink = $(event.target);
+ paneId = currentLink.attr("href");
+ currentPane = this.$editModePanes.filter(paneId);
+ this.$editModeLinks.parent().removeClass("active hover");
+ currentLink.parent().addClass("active hover");
+ this.$editModePanes.hide();
+ currentPane.fadeIn(200);
+ if (paneId === "#preview") {
+ return $.post(currentLink.data("preview-url"), {
+ content: this.editor.getValue()
+ }, function(response) {
+ currentPane.empty().append(response);
+ return currentPane.syntaxHighlight();
+ });
+ } else {
+ return this.editor.focus();
+ }
+ };
+
+ return EditBlob;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
new file mode 100644
index 00000000000..a612cf0f1ae
--- /dev/null
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -0,0 +1,57 @@
+//= require vue
+//= require vue-resource
+//= require Sortable
+//= require_tree ./models
+//= require_tree ./stores
+//= require_tree ./services
+//= require_tree ./mixins
+//= require ./components/board
+//= require ./components/new_list_dropdown
+//= require ./vue_resource_interceptor
+
+$(() => {
+ const $boardApp = document.getElementById('board-app'),
+ Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+
+ if (gl.IssueBoardsApp) {
+ gl.IssueBoardsApp.$destroy(true);
+ }
+
+ gl.IssueBoardsApp = new Vue({
+ el: $boardApp,
+ components: {
+ 'board': gl.issueBoards.Board
+ },
+ data: {
+ state: Store.state,
+ loading: true,
+ endpoint: $boardApp.dataset.endpoint,
+ disabled: $boardApp.dataset.disabled === 'true',
+ issueLinkBase: $boardApp.dataset.issueLinkBase
+ },
+ init: Store.create.bind(Store),
+ created () {
+ gl.boardService = new BoardService(this.endpoint);
+ },
+ ready () {
+ Store.disabled = this.disabled;
+ gl.boardService.all()
+ .then((resp) => {
+ resp.json().forEach((board) => {
+ const list = Store.addList(board);
+
+ if (list.type === 'done') {
+ list.position = Infinity;
+ } else if (list.type === 'backlog') {
+ list.position = -1;
+ }
+ });
+
+ Store.addBlankState();
+ this.loading = false;
+ });
+ }
+ });
+});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
new file mode 100644
index 00000000000..d7f4107cb02
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -0,0 +1,81 @@
+//= require ./board_blank_state
+//= require ./board_delete
+//= require ./board_list
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.Board = Vue.extend({
+ components: {
+ 'board-list': gl.issueBoards.BoardList,
+ 'board-delete': gl.issueBoards.BoardDelete,
+ 'board-blank-state': gl.issueBoards.BoardBlankState
+ },
+ props: {
+ list: Object,
+ disabled: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ query: '',
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ query () {
+ this.list.filters = this.getFilterData();
+ this.list.getIssues(true);
+ },
+ filters: {
+ handler () {
+ this.list.page = 1;
+ this.list.getIssues(true);
+ },
+ deep: true
+ }
+ },
+ methods: {
+ getFilterData () {
+ const filters = this.filters;
+ let queryData = { search: this.query };
+
+ Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
+
+ return queryData;
+ }
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ disabled: this.disabled,
+ group: 'boards',
+ draggable: '.is-draggable',
+ handle: '.js-board-handle',
+ onEnd: (e) => {
+ gl.issueBoards.onEnd();
+
+ 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);
+
+ this.$nextTick(() => {
+ Store.state.lists.splice(e.newIndex, 0, list);
+ Store.moveList(list, order);
+ });
+ }
+ }
+ });
+
+ 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
new file mode 100644
index 00000000000..63d72d857d9
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -0,0 +1,49 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardBlankState = Vue.extend({
+ data () {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'Development', color: '#5CB85C' }),
+ new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Production', color: '#FF5F00' }),
+ new ListLabel({ title: 'Ready', color: '#FF0000' })
+ ]
+ }
+ },
+ methods: {
+ addDefaultLists () {
+ this.clearBlankState();
+
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
+ title: label.title,
+ color: label.color
+ }
+ });
+ });
+
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
+
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
+ });
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store)
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
new file mode 100644
index 00000000000..4a7cfeaeab2
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -0,0 +1,43 @@
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardCard = Vue.extend({
+ props: {
+ list: Object,
+ issue: Object,
+ issueLinkBase: String,
+ disabled: Boolean,
+ index: Number
+ },
+ methods: {
+ filterByLabel (label, e) {
+ let labelToggleText = label.title;
+ const labelIndex = Store.state.filters['label_name'].indexOf(label.title);
+ $(e.target).tooltip('hide');
+
+ if (labelIndex === -1) {
+ Store.state.filters['label_name'].push(label.title);
+ $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`);
+ } else {
+ Store.state.filters['label_name'].splice(labelIndex, 1);
+ labelToggleText = Store.state.filters['label_name'][0];
+ $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove();
+ }
+
+ const selectedLabels = Store.state.filters['label_name'];
+ if (selectedLabels.length === 0) {
+ labelToggleText = 'Label';
+ } else if (selectedLabels.length > 1) {
+ labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`;
+ }
+
+ $('.labels-filter .dropdown-toggle-text').text(labelToggleText);
+
+ Store.updateFiltersUrl();
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
new file mode 100644
index 00000000000..34653cd48ef
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -0,0 +1,19 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardDelete = Vue.extend({
+ props: {
+ list: Object
+ },
+ methods: {
+ deleteBoard () {
+ $(this.$el).tooltip('hide');
+
+ if (confirm('Are you sure you want to delete this list?')) {
+ this.list.destroy();
+ }
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
new file mode 100644
index 00000000000..a6644e9eb8c
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -0,0 +1,87 @@
+//= require ./board_card
+
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardList = Vue.extend({
+ components: {
+ 'board-card': gl.issueBoards.BoardCard
+ },
+ props: {
+ disabled: Boolean,
+ list: Object,
+ issues: Array,
+ loading: Boolean,
+ issueLinkBase: String
+ },
+ data () {
+ return {
+ scrollOffset: 250,
+ filters: Store.state.filters
+ };
+ },
+ watch: {
+ filters: {
+ handler () {
+ this.list.loadingMore = false;
+ this.$els.list.scrollTop = 0;
+ },
+ deep: true
+ }
+ },
+ methods: {
+ listHeight () {
+ return this.$els.list.getBoundingClientRect().height;
+ },
+ scrollHeight () {
+ return this.$els.list.scrollHeight;
+ },
+ scrollTop () {
+ return this.$els.list.scrollTop + this.listHeight();
+ },
+ loadNextPage () {
+ const getIssues = this.list.nextPage();
+
+ if (getIssues) {
+ this.list.loadingMore = true;
+ getIssues.then(() => {
+ this.list.loadingMore = false;
+ });
+ }
+ },
+ },
+ ready () {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ group: 'issues',
+ sort: false,
+ disabled: this.disabled,
+ onStart: (e) => {
+ const card = this.$refs.issue[e.oldIndex];
+
+ Store.moving.issue = card.issue;
+ Store.moving.list = card.list;
+
+ gl.issueBoards.onStart();
+ },
+ onAdd: (e) => {
+ gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
+ },
+ onRemove: (e) => {
+ this.$refs.issue[e.oldIndex].$destroy(true);
+ }
+ });
+
+ this.sortable = Sortable.create(this.$els.list, options);
+
+ // Scroll event on list to load more
+ this.$els.list.onscroll = () => {
+ if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ this.loadNextPage();
+ }
+ };
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
new file mode 100644
index 00000000000..1a4d8157970
--- /dev/null
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -0,0 +1,54 @@
+$(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ $('.js-new-board-list').each(function () {
+ const $this = $(this);
+
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+
+ $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,
+ 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,
+ title: label.title,
+ color: label.color
+ }
+ });
+ }
+ }
+ });
+ });
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
new file mode 100644
index 00000000000..44addb3ea98
--- /dev/null
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -0,0 +1,35 @@
+((w) => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.onStart = () => {
+ $('.has-tooltip').tooltip('hide')
+ .tooltip('disable');
+ document.body.classList.add('is-dragging');
+ };
+
+ gl.issueBoards.onEnd = () => {
+ $('.has-tooltip').tooltip('enable');
+ document.body.classList.remove('is-dragging');
+ };
+
+ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+
+ gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+ let defaultSortOptions = {
+ forceFallback: true,
+ fallbackClass: 'is-dragging',
+ fallbackOnBody: true,
+ ghostClass: 'is-ghost',
+ filter: '.has-tooltip',
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
+ scrollSpeed: 20,
+ onStart: gl.issueBoards.onStart,
+ onEnd: gl.issueBoards.onEnd
+ }
+
+ Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+ return defaultSortOptions;
+ };
+})(window);
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
new file mode 100644
index 00000000000..eb082103de9
--- /dev/null
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -0,0 +1,44 @@
+class ListIssue {
+ constructor (obj) {
+ this.id = obj.iid;
+ this.title = obj.title;
+ this.confidential = obj.confidential;
+ this.labels = [];
+
+ if (obj.assignee) {
+ this.assignee = new ListUser(obj.assignee);
+ }
+
+ obj.labels.forEach((label) => {
+ this.labels.push(new ListLabel(label));
+ });
+
+ this.priority = this.labels.reduce((max, label) => {
+ return (label.priority < max) ? label.priority : max;
+ }, Infinity);
+ }
+
+ addLabel (label) {
+ if (!this.findLabel(label)) {
+ this.labels.push(new ListLabel(label));
+ }
+ }
+
+ findLabel (findLabel) {
+ return this.labels.filter( label => label.title === findLabel.title )[0];
+ }
+
+ removeLabel (removeLabel) {
+ if (removeLabel) {
+ this.labels = this.labels.filter( label => removeLabel.title !== label.title );
+ }
+ }
+
+ removeLabels (labels) {
+ labels.forEach(this.removeLabel.bind(this));
+ }
+
+ getLists () {
+ return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
+ }
+}
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
new file mode 100644
index 00000000000..583829552cd
--- /dev/null
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -0,0 +1,10 @@
+class ListLabel {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ this.color = obj.color;
+ this.textColor = obj.text_color;
+ this.description = obj.description;
+ this.priority = (obj.priority !== null) ? obj.priority : Infinity;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
new file mode 100644
index 00000000000..be2b8c568a8
--- /dev/null
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -0,0 +1,125 @@
+class List {
+ constructor (obj) {
+ this.id = obj.id;
+ this._uid = this.guid();
+ this.position = obj.position;
+ this.title = obj.title;
+ this.type = obj.list_type;
+ this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1;
+ this.filters = gl.issueBoards.BoardsStore.state.filters;
+ this.page = 1;
+ this.loading = true;
+ this.loadingMore = false;
+ this.issues = [];
+
+ if (obj.label) {
+ this.label = new ListLabel(obj.label);
+ }
+
+ if (this.type !== 'blank' && this.id) {
+ this.getIssues();
+ }
+ }
+
+ guid() {
+ const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
+ return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
+ }
+
+ save () {
+ return gl.boardService.createList(this.label.id)
+ .then((resp) => {
+ const data = resp.json();
+
+ this.id = data.id;
+ this.type = data.list_type;
+ this.position = data.position;
+
+ return this.getIssues();
+ });
+ }
+
+ destroy () {
+ gl.issueBoards.BoardsStore.state.lists.$remove(this);
+ gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
+
+ gl.boardService.destroyList(this.id);
+ }
+
+ update () {
+ gl.boardService.updateList(this.id, this.position);
+ }
+
+ nextPage () {
+ if (Math.floor(this.issues.length / 20) === this.page) {
+ this.page++;
+
+ return this.getIssues(false);
+ }
+ }
+
+ canSearch () {
+ return this.type === 'backlog';
+ }
+
+ getIssues (emptyIssues = true) {
+ const filters = this.filters;
+ let data = { page: this.page };
+
+ Object.keys(filters).forEach((key) => { data[key] = filters[key]; });
+
+ if (this.label) {
+ data.label_name = data.label_name.filter( label => label !== this.label.title );
+ }
+
+ if (emptyIssues) {
+ this.loading = true;
+ }
+
+ return gl.boardService.getIssuesForList(this.id, data)
+ .then((resp) => {
+ const data = resp.json();
+ this.loading = false;
+
+ if (emptyIssues) {
+ this.issues = [];
+ }
+
+ this.createIssues(data);
+ });
+ }
+
+ createIssues (data) {
+ data.forEach((issueObj) => {
+ this.addIssue(new ListIssue(issueObj));
+ });
+ }
+
+ addIssue (issue, listFrom) {
+ this.issues.push(issue);
+
+ if (this.label) {
+ issue.addLabel(this.label);
+ }
+
+ if (listFrom) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id);
+ }
+ }
+
+ findIssue (id) {
+ return this.issues.filter( issue => issue.id === id )[0];
+ }
+
+ removeIssue (removeIssue) {
+ this.issues = this.issues.filter((issue) => {
+ const matchesRemove = removeIssue.id === issue.id;
+
+ if (matchesRemove) {
+ issue.removeLabel(this.label);
+ }
+
+ return !matchesRemove;
+ });
+ }
+}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
new file mode 100644
index 00000000000..904b3a68507
--- /dev/null
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -0,0 +1,8 @@
+class ListUser {
+ constructor (user) {
+ this.id = user.id;
+ this.name = user.name;
+ this.username = user.username;
+ this.avatar = user.avatar_url;
+ }
+}
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
new file mode 100644
index 00000000000..9b80fb2e99f
--- /dev/null
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -0,0 +1,61 @@
+class BoardService {
+ constructor (root) {
+ Vue.http.options.root = root;
+
+ this.lists = Vue.resource(`${root}/lists{/id}`, {}, {
+ generate: {
+ method: 'POST',
+ url: `${root}/lists/generate.json`
+ }
+ });
+ this.issue = Vue.resource(`${root}/issues{/id}`, {});
+ this.issues = Vue.resource(`${root}/lists{/id}/issues`, {});
+
+ Vue.http.interceptors.push((request, next) => {
+ request.headers['X-CSRF-Token'] = $.rails.csrfToken();
+ next();
+ });
+ }
+
+ all () {
+ return this.lists.get();
+ }
+
+ generateDefaultLists () {
+ return this.lists.generate({});
+ }
+
+ createList (label_id) {
+ return this.lists.save({}, {
+ list: {
+ label_id
+ }
+ });
+ }
+
+ updateList (id, position) {
+ return this.lists.update({ id }, {
+ list: {
+ position
+ }
+ });
+ }
+
+ destroyList (id) {
+ return this.lists.delete({ id });
+ }
+
+ getIssuesForList (id, filter = {}) {
+ let data = { id };
+ Object.keys(filter).forEach((key) => { data[key] = filter[key]; });
+
+ return this.issues.get(data);
+ }
+
+ moveIssue (id, from_list_id, to_list_id) {
+ return this.issue.update({ id }, {
+ from_list_id,
+ to_list_id
+ });
+ }
+};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
new file mode 100644
index 00000000000..18f26a1f911
--- /dev/null
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -0,0 +1,112 @@
+(() => {
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardsStore = {
+ disabled: false,
+ state: {},
+ moving: {
+ issue: {},
+ list: {}
+ },
+ create () {
+ this.state.lists = [];
+ this.state.filters = {
+ author_id: gl.utils.getParameterValues('author_id')[0],
+ assignee_id: gl.utils.getParameterValues('assignee_id')[0],
+ milestone_title: gl.utils.getParameterValues('milestone_title')[0],
+ label_name: gl.utils.getParameterValues('label_name[]')
+ };
+ },
+ addList (listObj) {
+ const list = new List(listObj);
+ this.state.lists.push(list);
+
+ return list;
+ },
+ new (listObj) {
+ const list = this.addList(listObj),
+ backlogList = this.findList('type', 'backlog', 'backlog');
+
+ list
+ .save()
+ .then(() => {
+ // Remove any new issues from the backlog
+ // as they will be visible in the new list
+ list.issues.forEach(backlogList.removeIssue.bind(backlogList));
+ });
+ this.removeBlankState();
+ },
+ updateNewListDropdown (listId) {
+ $(`.js-board-list-${listId}`).removeClass('is-active');
+ },
+ shouldAddBlankState () {
+ // Decide whether to add the blank state
+ return !(this.state.lists.filter( list => list.type !== 'backlog' && list.type !== 'done' )[0]);
+ },
+ addBlankState () {
+ if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
+
+ this.addList({
+ id: 'blank',
+ list_type: 'blank',
+ title: 'Welcome to your Issue Board!',
+ position: 0
+ });
+ },
+ removeBlankState () {
+ this.removeList('blank');
+
+ $.cookie('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10
+ });
+ },
+ welcomeIsHidden () {
+ return $.cookie('issue_board_welcome_hidden') === 'true';
+ },
+ removeList (id, type = 'blank') {
+ const list = this.findList('id', id, type);
+
+ if (!list) return;
+
+ this.state.lists = this.state.lists.filter( list => list.id !== id );
+ },
+ moveList (listFrom, orderLists) {
+ orderLists.forEach((id, i) => {
+ const list = this.findList('id', parseInt(id));
+
+ list.position = i;
+ });
+ listFrom.update();
+ },
+ moveIssueToList (listFrom, listTo, issue) {
+ 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);
+ }
+
+ if (listTo.type === 'done' && listFrom.type !== 'backlog') {
+ issueLists.forEach((list) => {
+ list.removeIssue(issue);
+ })
+ issue.removeLabels(listLabels);
+ } else {
+ listFrom.removeIssue(issue);
+ }
+ },
+ findList (key, val, type = 'label') {
+ return this.state.lists.filter((list) => {
+ const byType = type ? list['type'] === type : true;
+
+ return list[key] === val && byType;
+ })[0];
+ },
+ updateFiltersUrl () {
+ history.pushState(null, null, `?${$.param(this.state.filters)}`);
+ }
+ };
+})();
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
new file mode 100644
index 00000000000..75f8b730195
--- /dev/null
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -0,0 +1,119 @@
+(function () {
+ 'use strict';
+
+ function simulateEvent(el, type, options) {
+ var event;
+ if (!el) return;
+ var ownerDocument = el.ownerDocument;
+
+ options = options || {};
+
+ if (/^mouse/.test(type)) {
+ event = ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData: function (type, val) {
+ this.data[type] = val;
+ },
+
+ getData: function (type) {
+ return this.data[type];
+ }
+ };
+ }
+
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent('on' + type, event);
+ }
+
+ return event;
+ }
+
+ function getTraget(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1]
+ );
+ }
+
+ function getRect(el) {
+ var rect = el.getBoundingClientRect();
+ var width = rect.right - rect.left;
+ var height = rect.bottom - rect.top;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + width / 2,
+ cy: rect.top + height / 2,
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2
+ };
+ }
+
+ function simulateDrag(options, callback) {
+ options.to.el = options.to.el || options.from.el;
+
+ var fromEl = getTraget(options.from);
+ var toEl = getTraget(options.to);
+ var scrollable = options.scrollable;
+
+ var fromRect = getRect(fromEl);
+ var toRect = getRect(toEl);
+
+ var startTime = new Date().getTime();
+ var duration = options.duration || 1000;
+ simulateEvent(fromEl, 'mousedown', {button: 0});
+ options.ontap && options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ var dragInterval = setInterval(function loop() {
+ var progress = (new Date().getTime() - startTime) / duration;
+ var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
+ var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
+ var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
+
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y
+ });
+
+ if (progress >= 1) {
+ options.ondragend && options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode
+ };
+ }
+
+
+ // Export
+ window.simulateEvent = simulateEvent;
+ window.simulateDrag = simulateDrag;
+})();
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
new file mode 100644
index 00000000000..f9f9f7999d4
--- /dev/null
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -0,0 +1,10 @@
+Vue.http.interceptors.push((request, next) => {
+ Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
+
+ Vue.nextTick(() => {
+ setTimeout(() => {
+ Vue.activeResources--;
+ }, 500);
+ });
+ next();
+});
diff --git a/app/assets/javascripts/breakpoints.coffee b/app/assets/javascripts/breakpoints.coffee
deleted file mode 100644
index 5457430f921..00000000000
--- a/app/assets/javascripts/breakpoints.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-class @Breakpoints
- instance = null;
-
- class BreakpointInstance
- BREAKPOINTS = ["xs", "sm", "md", "lg"]
-
- constructor: ->
- @setup()
-
- setup: ->
- allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
- ".device-#{breakpoint}"
- return if $(allDeviceSelector.join(",")).length
-
- # Create all the elements
- els = $.map BREAKPOINTS, (breakpoint) ->
- "<div class='device-#{breakpoint} visible-#{breakpoint}'></div>"
- $("body").append els.join('')
-
- visibleDevice: ->
- allDeviceSelector = BREAKPOINTS.map (breakpoint) ->
- ".device-#{breakpoint}"
- $(allDeviceSelector.join(",")).filter(":visible")
-
- getBreakpointSize: ->
- $visibleDevice = @visibleDevice
- # the page refreshed via turbolinks
- if not $visibleDevice().length
- @setup()
- $visibleDevice = @visibleDevice()
- return $visibleDevice.attr("class").split("visible-")[1]
-
- @get: ->
- return instance ?= new BreakpointInstance
-
-$ =>
- @bp = Breakpoints.get()
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
new file mode 100644
index 00000000000..1e0148e5798
--- /dev/null
+++ b/app/assets/javascripts/breakpoints.js
@@ -0,0 +1,68 @@
+(function() {
+ this.Breakpoints = (function() {
+ var BreakpointInstance, instance;
+
+ function Breakpoints() {}
+
+ instance = null;
+
+ BreakpointInstance = (function() {
+ var BREAKPOINTS;
+
+ BREAKPOINTS = ["xs", "sm", "md", "lg"];
+
+ function BreakpointInstance() {
+ this.setup();
+ }
+
+ BreakpointInstance.prototype.setup = function() {
+ var allDeviceSelector, els;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ if ($(allDeviceSelector.join(",")).length) {
+ return;
+ }
+ els = $.map(BREAKPOINTS, function(breakpoint) {
+ return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
+ });
+ return $("body").append(els.join(''));
+ };
+
+ BreakpointInstance.prototype.visibleDevice = function() {
+ var allDeviceSelector;
+ allDeviceSelector = BREAKPOINTS.map(function(breakpoint) {
+ return ".device-" + breakpoint;
+ });
+ return $(allDeviceSelector.join(",")).filter(":visible");
+ };
+
+ BreakpointInstance.prototype.getBreakpointSize = function() {
+ var $visibleDevice;
+ $visibleDevice = this.visibleDevice;
+ if (!$visibleDevice().length) {
+ this.setup();
+ }
+ $visibleDevice = this.visibleDevice();
+ return $visibleDevice.attr("class").split("visible-")[1];
+ };
+
+ return BreakpointInstance;
+
+ })();
+
+ Breakpoints.get = function() {
+ return instance != null ? instance : instance = new BreakpointInstance;
+ };
+
+ return Breakpoints;
+
+ })();
+
+ $((function(_this) {
+ return function() {
+ return _this.bp = Breakpoints.get();
+ };
+ })(this));
+
+}).call(this);
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
new file mode 100644
index 00000000000..fceeff36728
--- /dev/null
+++ b/app/assets/javascripts/broadcast_message.js
@@ -0,0 +1,34 @@
+(function() {
+ $(function() {
+ var previewPath;
+ $('input#broadcast_message_color').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('background-color', previewColor);
+ });
+ $('input#broadcast_message_font').on('input', function() {
+ var previewColor;
+ previewColor = $(this).val();
+ return $('div.broadcast-message-preview').css('color', previewColor);
+ });
+ previewPath = $('textarea#broadcast_message_message').data('preview-path');
+ return $('textarea#broadcast_message_message').on('input', function() {
+ var message;
+ message = $(this).val();
+ if (message === '') {
+ return $('.js-broadcast-message-preview').text("Your message here");
+ } else {
+ return $.ajax({
+ url: previewPath,
+ type: "POST",
+ data: {
+ broadcast_message: {
+ message: message
+ }
+ }
+ });
+ }
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/broadcast_message.js.coffee b/app/assets/javascripts/broadcast_message.js.coffee
deleted file mode 100644
index a38a329c4c2..00000000000
--- a/app/assets/javascripts/broadcast_message.js.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-$ ->
- $('input#broadcast_message_color').on 'input', ->
- previewColor = $(@).val()
- $('div.broadcast-message-preview').css('background-color', previewColor)
-
- $('input#broadcast_message_font').on 'input', ->
- previewColor = $(@).val()
- $('div.broadcast-message-preview').css('color', previewColor)
-
- previewPath = $('textarea#broadcast_message_message').data('preview-path')
-
- $('textarea#broadcast_message_message').on 'input', ->
- message = $(@).val()
-
- if message == ''
- $('.js-broadcast-message-preview').text("Your message here")
- else
- $.ajax(
- url: previewPath
- type: "POST"
- data: { broadcast_message: { message: message } }
- )
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
new file mode 100644
index 00000000000..4d066f13646
--- /dev/null
+++ b/app/assets/javascripts/build.js
@@ -0,0 +1,164 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Build = (function() {
+ Build.interval = null;
+
+ 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);
+ this.updateDropdown = bind(this.updateDropdown, this);
+ clearInterval(Build.interval);
+ this.bp = Breakpoints.get();
+ $('.js-build-sidebar').niceScroll();
+
+ this.populateJobs(this.build_stage);
+ this.updateStageDropdownText(this.build_stage);
+ this.hideSidebar();
+
+ $(document).off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.toggleSidebar);
+ $(window).off('resize.build').on('resize.build', this.hideSidebar);
+ $(document).off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
+ this.updateArtifactRemoveDate();
+ if ($('#build-trace').length) {
+ this.getInitialBuildTrace();
+ this.initScrollButtonAffix();
+ }
+ if (this.build_status === "running" || this.build_status === "pending") {
+ $('#autoscroll-button').on('click', function() {
+ var state;
+ state = $(this).data("state");
+ if ("enabled" === state) {
+ $(this).data("state", "disabled");
+ return $(this).text("enable autoscroll");
+ } else {
+ $(this).data("state", "enabled");
+ return $(this).text("disable autoscroll");
+ }
+ });
+ Build.interval = setInterval((function(_this) {
+ return function() {
+ if (window.location.href.split("#").first() === _this.page_url) {
+ return _this.getBuildTrace();
+ }
+ };
+ })(this), 4000);
+ }
+ }
+
+ Build.prototype.getInitialBuildTrace = function() {
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+
+ return $.ajax({
+ url: this.build_url,
+ dataType: 'json',
+ success: function(build_data) {
+ $('.js-build-output').html(build_data.trace_html);
+ if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
+ return $('.js-build-refresh').remove();
+ }
+ }
+ });
+ };
+
+ Build.prototype.getBuildTrace = function() {
+ return $.ajax({
+ url: this.page_url + "/trace.json?state=" + (encodeURIComponent(this.state)),
+ dataType: "json",
+ success: (function(_this) {
+ return function(log) {
+ if (log.state) {
+ _this.state = log.state;
+ }
+ if (log.status === "running") {
+ if (log.append) {
+ $('.js-build-output').append(log.html);
+ } else {
+ $('.js-build-output').html(log.html);
+ }
+ return _this.checkAutoscroll();
+ } else if (log.status !== _this.build_status) {
+ return Turbolinks.visit(_this.page_url);
+ }
+ };
+ })(this)
+ });
+ };
+
+ Build.prototype.checkAutoscroll = function() {
+ if ("enabled" === $("#autoscroll-button").data("state")) {
+ return $("html,body").scrollTop($("#build-trace").height());
+ }
+ };
+
+ Build.prototype.initScrollButtonAffix = function() {
+ var $body, $buildScroll, $buildTrace;
+ $buildScroll = $('#js-build-scroll');
+ $body = $('body');
+ $buildTrace = $('#build-trace');
+ return $buildScroll.affix({
+ offset: {
+ bottom: function() {
+ return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
+ }
+ }
+ });
+ };
+
+ Build.prototype.shouldHideSidebar = function() {
+ var bootstrapBreakpoint;
+ bootstrapBreakpoint = this.bp.getBreakpointSize();
+ return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
+ };
+
+ Build.prototype.toggleSidebar = function() {
+ if (this.shouldHideSidebar()) {
+ return $('.js-build-sidebar').toggleClass('right-sidebar-expanded right-sidebar-collapsed');
+ }
+ };
+
+ Build.prototype.hideSidebar = function() {
+ if (this.shouldHideSidebar()) {
+ return $('.js-build-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ return $('.js-build-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ }
+ };
+
+ Build.prototype.updateArtifactRemoveDate = function() {
+ var $date, date;
+ $date = $('.js-artifacts-remove');
+ if ($date.length) {
+ date = $date.text();
+ return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
+ }
+ };
+
+ Build.prototype.populateJobs = function(stage) {
+ $('.build-job').hide();
+ $('.build-job[data-stage="' + stage + '"]').show();
+ };
+
+ Build.prototype.updateStageDropdownText = function(stage) {
+ $('.stage-selection').text(stage);
+ };
+
+ Build.prototype.updateDropdown = function(e) {
+ e.preventDefault();
+ var stage = e.currentTarget.text;
+ this.updateStageDropdownText(stage);
+ this.populateJobs(stage);
+ };
+
+ return Build;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
new file mode 100644
index 00000000000..f345ba0abe6
--- /dev/null
+++ b/app/assets/javascripts/build_artifacts.js
@@ -0,0 +1,27 @@
+(function() {
+ this.BuildArtifacts = (function() {
+ function BuildArtifacts() {
+ this.disablePropagation();
+ this.setupEntryClick();
+ }
+
+ BuildArtifacts.prototype.disablePropagation = function() {
+ $('.top-block').on('click', '.download', function(e) {
+ return e.stopPropagation();
+ });
+ return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
+ return e.stopImmediatePropagation();
+ });
+ };
+
+ BuildArtifacts.prototype.setupEntryClick = function() {
+ return $('.tree-holder').on('click', 'tr[data-link]', function(e) {
+ return window.location = this.dataset.link;
+ });
+ };
+
+ return BuildArtifacts;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/build_artifacts.js.coffee b/app/assets/javascripts/build_artifacts.js.coffee
deleted file mode 100644
index 5ae6cba56c8..00000000000
--- a/app/assets/javascripts/build_artifacts.js.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-class @BuildArtifacts
- constructor: () ->
- @disablePropagation()
- @setupEntryClick()
-
- disablePropagation: ->
- $('.top-block').on 'click', '.download', (e) ->
- e.stopPropagation()
- $('.tree-holder').on 'click', 'tr[data-link] a', (e) ->
- e.stopImmediatePropagation()
-
- setupEntryClick: ->
- $('.tree-holder').on 'click', 'tr[data-link]', (e) ->
- window.location = @dataset.link
diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee
deleted file mode 100644
index ca24c1d759f..00000000000
--- a/app/assets/javascripts/ci/application.js.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-#= require pager
-#= require jquery_nested_form
-#= require_tree .
-
-$(document).on 'click', '.assign-all-runner', ->
- $(this).replaceWith('<i class="fa fa-refresh fa-spin"></i> Assign in progress..')
-
-window.unbindEvents = ->
- $(document).unbind('scroll')
- $(document).off('scroll')
-
-document.addEventListener("page:fetch", unbindEvents)
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
deleted file mode 100644
index 26b8686dee8..00000000000
--- a/app/assets/javascripts/ci/build.coffee
+++ /dev/null
@@ -1,116 +0,0 @@
-class @CiBuild
- @interval: null
- @state: null
-
- constructor: (@page_url, @build_url, @build_status, @state) ->
- clearInterval(CiBuild.interval)
-
- # Init breakpoint checker
- @bp = Breakpoints.get()
- @hideSidebar()
- $('.js-build-sidebar').niceScroll()
- $(document)
- .off 'click', '.js-sidebar-build-toggle'
- .on 'click', '.js-sidebar-build-toggle', @toggleSidebar
-
- $(window)
- .off 'resize.build'
- .on 'resize.build', @hideSidebar
-
- @updateArtifactRemoveDate()
-
- if $('#build-trace').length
- @getInitialBuildTrace()
- @initScrollButtonAffix()
-
- if @build_status is "running" or @build_status is "pending"
- #
- # Bind autoscroll button to follow build output
- #
- $('#autoscroll-button').on 'click', ->
- state = $(this).data("state")
- if "enabled" is state
- $(this).data "state", "disabled"
- $(this).text "enable autoscroll"
- else
- $(this).data "state", "enabled"
- $(this).text "disable autoscroll"
-
- #
- # Check for new build output if user still watching build page
- # Only valid for runnig build when output changes during time
- #
- CiBuild.interval = setInterval =>
- if window.location.href.split("#").first() is @page_url
- @getBuildTrace()
- , 4000
-
- getInitialBuildTrace: ->
- removeRefreshStatuses = ['success', 'failed', 'canceled']
-
- $.ajax
- url: @build_url
- dataType: 'json'
- success: (build_data) ->
- $('.js-build-output').html build_data.trace_html
-
- if removeRefreshStatuses.indexOf(build_data.status) >= 0
- $('.js-build-refresh').remove()
-
- getBuildTrace: ->
- $.ajax
- url: "#{@page_url}/trace.json?state=#{encodeURIComponent(@state)}"
- dataType: "json"
- success: (log) =>
- if log.state
- @state = log.state
-
- if log.status is "running"
- if log.append
- $('.js-build-output').append log.html
- else
- $('.js-build-output').html log.html
- @checkAutoscroll()
- else if log.status isnt @build_status
- Turbolinks.visit @page_url
-
- checkAutoscroll: ->
- $("html,body").scrollTop $("#build-trace").height() if "enabled" is $("#autoscroll-button").data("state")
-
- initScrollButtonAffix: ->
- $buildScroll = $('#js-build-scroll')
- $body = $('body')
- $buildTrace = $('#build-trace')
-
- $buildScroll.affix(
- offset:
- bottom: ->
- $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top)
- )
-
- shouldHideSidebar: ->
- bootstrapBreakpoint = @bp.getBreakpointSize()
-
- bootstrapBreakpoint is 'xs' or bootstrapBreakpoint is 'sm'
-
- toggleSidebar: =>
- if @shouldHideSidebar()
- $('.js-build-sidebar')
- .toggleClass 'right-sidebar-expanded right-sidebar-collapsed'
-
- hideSidebar: =>
- if @shouldHideSidebar()
- $('.js-build-sidebar')
- .removeClass 'right-sidebar-expanded'
- .addClass 'right-sidebar-collapsed'
- else
- $('.js-build-sidebar')
- .removeClass 'right-sidebar-collapsed'
- .addClass 'right-sidebar-expanded'
-
- updateArtifactRemoveDate: ->
- $date = $('.js-artifacts-remove')
-
- if $date.length
- date = $date.text()
- $date.text $.timefor(new Date(date), ' ')
diff --git a/app/assets/javascripts/ci/projects.js.coffee b/app/assets/javascripts/ci/projects.js.coffee
deleted file mode 100644
index e6406011d11..00000000000
--- a/app/assets/javascripts/ci/projects.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-$(document).on 'click', '.badge-codes-toggle', ->
- $('.badge-codes-block').toggleClass("hide")
- return false
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
new file mode 100644
index 00000000000..23cf5b519f4
--- /dev/null
+++ b/app/assets/javascripts/commit.js
@@ -0,0 +1,13 @@
+(function() {
+ this.Commit = (function() {
+ function Commit() {
+ $('.files .diff-file').each(function() {
+ return new CommitFile(this);
+ });
+ }
+
+ return Commit;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/commit.js.coffee b/app/assets/javascripts/commit.js.coffee
deleted file mode 100644
index 0566e239191..00000000000
--- a/app/assets/javascripts/commit.js.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-class @Commit
- constructor: ->
- $('.files .diff-file').each ->
- new CommitFile(this)
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
new file mode 100644
index 00000000000..be24ee56aad
--- /dev/null
+++ b/app/assets/javascripts/commit/file.js
@@ -0,0 +1,13 @@
+(function() {
+ this.CommitFile = (function() {
+ function CommitFile(file) {
+ if ($('.image', file).length) {
+ new ImageFile(file);
+ }
+ }
+
+ return CommitFile;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/commit/file.js.coffee b/app/assets/javascripts/commit/file.js.coffee
deleted file mode 100644
index 83e793863b6..00000000000
--- a/app/assets/javascripts/commit/file.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-class @CommitFile
-
- constructor: (file) ->
- if $('.image', file).length
- new ImageFile(file)
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js
new file mode 100644
index 00000000000..c0d0b2d049f
--- /dev/null
+++ b/app/assets/javascripts/commit/image-file.js
@@ -0,0 +1,175 @@
+(function() {
+ this.ImageFile = (function() {
+ var prepareFrames;
+
+ ImageFile.availWidth = 900;
+
+ ImageFile.viewModes = ['two-up', 'swipe'];
+
+ function ImageFile(file) {
+ this.file = file;
+ this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
+ return function(deletedWidth, deletedHeight) {
+ return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
+ if (width === deletedWidth && height === deletedHeight) {
+ return _this.initViewModes();
+ } else {
+ return _this.initView('two-up');
+ }
+ });
+ };
+ })(this));
+ }
+
+ ImageFile.prototype.initViewModes = function() {
+ var viewMode;
+ viewMode = ImageFile.viewModes[0];
+ $('.view-modes', this.file).removeClass('hide');
+ $('.view-modes-menu', this.file).on('click', 'li', (function(_this) {
+ return function(event) {
+ if (!$(event.currentTarget).hasClass('active')) {
+ return _this.activateViewMode(event.currentTarget.className);
+ }
+ };
+ })(this));
+ return this.activateViewMode(viewMode);
+ };
+
+ ImageFile.prototype.activateViewMode = function(viewMode) {
+ $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active');
+ return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) {
+ return function() {
+ $(".view." + viewMode, _this.file).fadeIn(200);
+ return _this.initView(viewMode);
+ };
+ })(this));
+ };
+
+ ImageFile.prototype.initView = function(viewMode) {
+ return this.views[viewMode].call(this);
+ };
+
+ prepareFrames = function(view) {
+ var maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ $('.frame', view).each((function(_this) {
+ return function(index, frame) {
+ var height, width;
+ width = $(frame).width();
+ height = $(frame).height();
+ maxWidth = width > maxWidth ? width : maxWidth;
+ return maxHeight = height > maxHeight ? height : maxHeight;
+ };
+ })(this)).css({
+ width: maxWidth,
+ height: maxHeight
+ });
+ return [maxWidth, maxHeight];
+ };
+
+ ImageFile.prototype.views = {
+ 'two-up': function() {
+ return $('.two-up.view .wrap', this.file).each((function(_this) {
+ return function(index, wrap) {
+ $('img', wrap).each(function() {
+ var currentWidth;
+ currentWidth = $(this).width();
+ if (currentWidth > ImageFile.availWidth / 2) {
+ return $(this).width(ImageFile.availWidth / 2);
+ }
+ });
+ return _this.requestImageInfo($('img', wrap), function(width, height) {
+ $('.image-info .meta-width', wrap).text(width + "px");
+ $('.image-info .meta-height', wrap).text(height + "px");
+ return $('.image-info', wrap).removeClass('hide');
+ });
+ };
+ })(this));
+ },
+ 'swipe': function() {
+ var maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ return $('.swipe.view', this.file).each((function(_this) {
+ return function(index, view) {
+ var ref;
+ ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ $('.swipe-frame', view).css({
+ width: maxWidth + 16,
+ height: maxHeight + 28
+ });
+ $('.swipe-wrap', view).css({
+ width: maxWidth + 1,
+ height: maxHeight + 2
+ });
+ return $('.swipe-bar', view).css({
+ left: 0
+ }).draggable({
+ axis: 'x',
+ containment: 'parent',
+ drag: function(event) {
+ return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
+ },
+ stop: function(event) {
+ return $('.swipe-wrap', view).width((maxWidth + 1) - $(this).position().left);
+ }
+ });
+ };
+ })(this));
+ },
+ 'onion-skin': function() {
+ var dragTrackWidth, maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
+ return $('.onion-skin.view', this.file).each((function(_this) {
+ return function(index, view) {
+ var ref;
+ ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ $('.onion-skin-frame', view).css({
+ width: maxWidth + 16,
+ height: maxHeight + 28
+ });
+ $('.swipe-wrap', view).css({
+ width: maxWidth + 1,
+ height: maxHeight + 2
+ });
+ return $('.dragger', view).css({
+ left: dragTrackWidth
+ }).draggable({
+ axis: 'x',
+ containment: 'parent',
+ drag: function(event) {
+ return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
+ },
+ stop: function(event) {
+ return $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth);
+ }
+ });
+ };
+ })(this));
+ }
+ };
+
+ ImageFile.prototype.requestImageInfo = function(img, callback) {
+ var domImg;
+ domImg = img.get(0);
+ if (domImg) {
+ if (domImg.complete) {
+ return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
+ } else {
+ return img.on('load', (function(_this) {
+ return function() {
+ return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
+ };
+ })(this));
+ }
+ }
+ };
+
+ return ImageFile;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee
deleted file mode 100644
index 9c723f51e54..00000000000
--- a/app/assets/javascripts/commit/image-file.js.coffee
+++ /dev/null
@@ -1,127 +0,0 @@
-class @ImageFile
-
- # Width where images must fits in, for 2-up this gets divided by 2
- @availWidth = 900
- @viewModes = ['two-up', 'swipe']
-
- constructor: (@file) ->
- # Determine if old and new file has same dimensions, if not show 'two-up' view
- this.requestImageInfo $('.two-up.view .frame.deleted img', @file), (deletedWidth, deletedHeight) =>
- this.requestImageInfo $('.two-up.view .frame.added img', @file), (width, height) =>
- if width == deletedWidth && height == deletedHeight
- this.initViewModes()
- else
- this.initView('two-up')
-
- initViewModes: ->
- viewMode = ImageFile.viewModes[0]
-
- $('.view-modes', @file).removeClass 'hide'
- $('.view-modes-menu', @file).on 'click', 'li', (event) =>
- unless $(event.currentTarget).hasClass('active')
- this.activateViewMode(event.currentTarget.className)
-
- this.activateViewMode(viewMode)
-
- activateViewMode: (viewMode) ->
- $('.view-modes-menu li', @file)
- .removeClass('active')
- .filter(".#{viewMode}").addClass 'active'
- $(".view:visible:not(.#{viewMode})", @file).fadeOut 200, =>
- $(".view.#{viewMode}", @file).fadeIn(200)
- this.initView viewMode
-
- initView: (viewMode) ->
- this.views[viewMode].call(this)
-
- prepareFrames = (view) ->
- maxWidth = 0
- maxHeight = 0
- $('.frame', view).each (index, frame) =>
- width = $(frame).width()
- height = $(frame).height()
- maxWidth = if width > maxWidth then width else maxWidth
- maxHeight = if height > maxHeight then height else maxHeight
- .css
- width: maxWidth
- height: maxHeight
-
- [maxWidth, maxHeight]
-
- views:
- 'two-up': ->
- $('.two-up.view .wrap', @file).each (index, wrap) =>
- $('img', wrap).each ->
- currentWidth = $(this).width()
- if currentWidth > ImageFile.availWidth / 2
- $(this).width ImageFile.availWidth / 2
-
- this.requestImageInfo $('img', wrap), (width, height) ->
- $('.image-info .meta-width', wrap).text "#{width}px"
- $('.image-info .meta-height', wrap).text "#{height}px"
- $('.image-info', wrap).removeClass('hide')
-
- 'swipe': ->
- maxWidth = 0
- maxHeight = 0
-
- $('.swipe.view', @file).each (index, view) =>
-
- [maxWidth, maxHeight] = prepareFrames(view)
-
- $('.swipe-frame', view).css
- width: maxWidth + 16
- height: maxHeight + 28
-
- $('.swipe-wrap', view).css
- width: maxWidth + 1
- height: maxHeight + 2
-
- $('.swipe-bar', view).css
- left: 0
- .draggable
- axis: 'x'
- containment: 'parent'
- drag: (event) ->
- $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left
- stop: (event) ->
- $('.swipe-wrap', view).width (maxWidth + 1) - $(this).position().left
-
- 'onion-skin': ->
- maxWidth = 0
- maxHeight = 0
-
- dragTrackWidth = $('.drag-track', @file).width() - $('.dragger', @file).width()
-
- $('.onion-skin.view', @file).each (index, view) =>
-
- [maxWidth, maxHeight] = prepareFrames(view)
-
- $('.onion-skin-frame', view).css
- width: maxWidth + 16
- height: maxHeight + 28
-
- $('.swipe-wrap', view).css
- width: maxWidth + 1
- height: maxHeight + 2
-
- $('.dragger', view).css
- left: dragTrackWidth
- .draggable
- axis: 'x'
- containment: 'parent'
- drag: (event) ->
- $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth)
- stop: (event) ->
- $('.frame.added', view).css('opacity', $(this).position().left / dragTrackWidth)
-
-
-
- requestImageInfo: (img, callback) ->
- domImg = img.get(0)
- if domImg
- if domImg.complete
- callback.call(this, domImg.naturalWidth, domImg.naturalHeight)
- else
- img.on 'load', =>
- callback.call(this, domImg.naturalWidth, domImg.naturalHeight)
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
new file mode 100644
index 00000000000..37f168c5190
--- /dev/null
+++ b/app/assets/javascripts/commits.js
@@ -0,0 +1,58 @@
+(function() {
+ this.CommitsList = (function() {
+ function CommitsList() {}
+
+ CommitsList.timer = null;
+
+ CommitsList.init = function(limit) {
+ $("body").on("click", ".day-commits-table li.commit", function(event) {
+ if (event.target.nodeName !== "A") {
+ location.href = $(this).attr("url");
+ e.stopPropagation();
+ return false;
+ }
+ });
+ Pager.init(limit, false);
+ this.content = $("#commits-list");
+ this.searchField = $("#commits-search");
+ return this.initSearch();
+ };
+
+ CommitsList.initSearch = function() {
+ this.timer = null;
+ return this.searchField.keyup((function(_this) {
+ return function() {
+ clearTimeout(_this.timer);
+ return _this.timer = setTimeout(_this.filterResults, 500);
+ };
+ })(this));
+ };
+
+ CommitsList.filterResults = function() {
+ var commitsUrl, form, search;
+ form = $(".commits-search-form");
+ search = CommitsList.searchField.val();
+ commitsUrl = form.attr("action") + '?' + form.serialize();
+ CommitsList.content.fadeTo('fast', 0.5);
+ return $.ajax({
+ type: "GET",
+ url: form.attr("action"),
+ data: form.serialize(),
+ complete: function() {
+ return CommitsList.content.fadeTo('fast', 1.0);
+ },
+ success: function(data) {
+ CommitsList.content.html(data.html);
+ return history.replaceState({
+ page: commitsUrl
+ }, document.title, commitsUrl);
+ },
+ dataType: "json"
+ });
+ };
+
+ return CommitsList;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee
deleted file mode 100644
index 0acb4c1955e..00000000000
--- a/app/assets/javascripts/commits.js.coffee
+++ /dev/null
@@ -1,39 +0,0 @@
-class @CommitsList
- @timer = null
-
- @init: (limit) ->
- $("body").on "click", ".day-commits-table li.commit", (event) ->
- if event.target.nodeName != "A"
- location.href = $(this).attr("url")
- e.stopPropagation()
- return false
-
- Pager.init limit, false
-
- @content = $("#commits-list")
- @searchField = $("#commits-search")
- @initSearch()
-
- @initSearch: ->
- @timer = null
- @searchField.keyup =>
- clearTimeout(@timer)
- @timer = setTimeout(@filterResults, 500)
-
- @filterResults: =>
- form = $(".commits-search-form")
- search = @searchField.val()
- commitsUrl = form.attr("action") + '?' + form.serialize()
- @content.fadeTo('fast', 0.5)
-
- $.ajax
- type: "GET"
- url: form.attr("action")
- data: form.serialize()
- complete: =>
- @content.fadeTo('fast', 1.0)
- success: (data) =>
- @content.html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: commitsUrl}, document.title, commitsUrl
- dataType: "json"
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
new file mode 100644
index 00000000000..342ac0e8e69
--- /dev/null
+++ b/app/assets/javascripts/compare.js
@@ -0,0 +1,91 @@
+(function() {
+ this.Compare = (function() {
+ function Compare(opts) {
+ this.opts = opts;
+ this.source_loading = $(".js-source-loading");
+ this.target_loading = $(".js-target-loading");
+ $('.js-compare-dropdown').each((function(_this) {
+ return function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return $dropdown.glDropdown({
+ selectable: true,
+ fieldName: $dropdown.data('field-name'),
+ filterable: true,
+ id: function(obj, $el) {
+ return $el.data('id');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(e, el) {
+ if ($dropdown.is('.js-target-branch')) {
+ return _this.getTargetHtml();
+ } else if ($dropdown.is('.js-source-branch')) {
+ return _this.getSourceHtml();
+ } else if ($dropdown.is('.js-target-project')) {
+ return _this.getTargetProject();
+ }
+ }
+ });
+ };
+ })(this));
+ this.initialState();
+ }
+
+ Compare.prototype.initialState = function() {
+ this.getSourceHtml();
+ return this.getTargetHtml();
+ };
+
+ Compare.prototype.getTargetProject = function() {
+ return $.ajax({
+ url: this.opts.targetProjectUrl,
+ data: {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val()
+ },
+ beforeSend: function() {
+ return $('.mr_target_commit').empty();
+ },
+ success: function(html) {
+ return $('.js-target-branch-dropdown .dropdown-content').html(html);
+ }
+ });
+ };
+
+ Compare.prototype.getSourceHtml = function() {
+ return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', {
+ ref: $("input[name='merge_request[source_branch]']").val()
+ });
+ };
+
+ Compare.prototype.getTargetHtml = function() {
+ return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', {
+ target_project_id: $("input[name='merge_request[target_project_id]']").val(),
+ ref: $("input[name='merge_request[target_branch]']").val()
+ });
+ };
+
+ Compare.prototype.sendAjax = function(url, loading, target, data) {
+ var $target;
+ $target = $(target);
+ return $.ajax({
+ url: url,
+ data: data,
+ beforeSend: function() {
+ loading.show();
+ return $target.empty();
+ },
+ success: function(html) {
+ loading.hide();
+ $target.html(html);
+ return $('.js-timeago', $target).timeago();
+ }
+ });
+ };
+
+ return Compare;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/compare.js.coffee b/app/assets/javascripts/compare.js.coffee
deleted file mode 100644
index f20992ead3e..00000000000
--- a/app/assets/javascripts/compare.js.coffee
+++ /dev/null
@@ -1,67 +0,0 @@
-class @Compare
- constructor: (@opts) ->
- @source_loading = $ ".js-source-loading"
- @target_loading = $ ".js-target-loading"
-
- $('.js-compare-dropdown').each (i, dropdown) =>
- $dropdown = $(dropdown)
-
- $dropdown.glDropdown(
- selectable: true
- fieldName: $dropdown.data 'field-name'
- filterable: true
- id: (obj, $el) ->
- $el.data 'id'
- toggleLabel: (obj, $el) ->
- $el.text().trim()
- clicked: (e, el) =>
- if $dropdown.is '.js-target-branch'
- @getTargetHtml()
- else if $dropdown.is '.js-source-branch'
- @getSourceHtml()
- else if $dropdown.is '.js-target-project'
- @getTargetProject()
- )
-
- @initialState()
-
- initialState: ->
- @getSourceHtml()
- @getTargetHtml()
-
- getTargetProject: ->
- $.ajax(
- url: @opts.targetProjectUrl
- data:
- target_project_id: $("input[name='merge_request[target_project_id]']").val()
- beforeSend: ->
- $('.mr_target_commit').empty()
- success: (html) ->
- $('.js-target-branch-dropdown .dropdown-content').html html
- )
-
- getSourceHtml: ->
- @sendAjax(@opts.sourceBranchUrl, @source_loading, '.mr_source_commit',
- ref: $("input[name='merge_request[source_branch]']").val()
- )
-
- getTargetHtml: ->
- @sendAjax(@opts.targetBranchUrl, @target_loading, '.mr_target_commit',
- target_project_id: $("input[name='merge_request[target_project_id]']").val()
- ref: $("input[name='merge_request[target_branch]']").val()
- )
-
- sendAjax: (url, loading, target, data) ->
- $target = $(target)
-
- $.ajax(
- url: url
- data: data
- beforeSend: ->
- loading.show()
- $target.empty()
- success: (html) ->
- loading.hide()
- $target.html html
- $('.js-timeago', $target).timeago()
- )
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
new file mode 100644
index 00000000000..4e3a28cd163
--- /dev/null
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -0,0 +1,51 @@
+(function() {
+ this.CompareAutocomplete = (function() {
+ function CompareAutocomplete() {
+ this.initDropdown();
+ }
+
+ CompareAutocomplete.prototype.initDropdown = function() {
+ return $('.js-compare-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref')
+ }
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterByText: true,
+ fieldName: $dropdown.attr('name'),
+ filterInput: 'input[type="text"]',
+ renderRow: function(ref) {
+ var link;
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ } else {
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ return $('<li />').append(link);
+ }
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ }
+ });
+ });
+ };
+
+ return CompareAutocomplete;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
new file mode 100644
index 00000000000..708ab08ffac
--- /dev/null
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -0,0 +1,32 @@
+(function() {
+ this.ConfirmDangerModal = (function() {
+ function ConfirmDangerModal(form, text) {
+ var project_path, submit;
+ this.form = form;
+ $('.js-confirm-text').text(text || '');
+ $('.js-confirm-danger-input').val('');
+ $('#modal-confirm-danger').modal('show');
+ project_path = $('.js-confirm-danger-match').text();
+ submit = $('.js-confirm-danger-submit');
+ submit.disable();
+ $('.js-confirm-danger-input').off('input');
+ $('.js-confirm-danger-input').on('input', function() {
+ if (rstrip($(this).val()) === project_path) {
+ return submit.enable();
+ } else {
+ return submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit').off('click');
+ $('.js-confirm-danger-submit').on('click', (function(_this) {
+ return function() {
+ return _this.form.submit();
+ };
+ })(this));
+ }
+
+ return ConfirmDangerModal;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee
deleted file mode 100644
index 66e34dd4a08..00000000000
--- a/app/assets/javascripts/confirm_danger_modal.js.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-class @ConfirmDangerModal
- constructor: (form, text) ->
- @form = form
- $('.js-confirm-text').text(text || '')
- $('.js-confirm-danger-input').val('')
- $('#modal-confirm-danger').modal('show')
- project_path = $('.js-confirm-danger-match').text()
- submit = $('.js-confirm-danger-submit')
- submit.disable()
-
- $('.js-confirm-danger-input').off 'input'
- $('.js-confirm-danger-input').on 'input', ->
- if rstrip($(@).val()) is project_path
- submit.enable()
- else
- submit.disable()
-
- $('.js-confirm-danger-submit').off 'click'
- $('.js-confirm-danger-submit').on 'click', =>
- @form.submit()
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
new file mode 100644
index 00000000000..c43af17442b
--- /dev/null
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -0,0 +1,43 @@
+
+/*= require clipboard */
+
+(function() {
+ var genericError, genericSuccess, showTooltip;
+
+ genericSuccess = function(e) {
+ showTooltip(e.trigger, 'Copied!');
+ e.clearSelection();
+ return $(e.trigger).blur();
+ };
+
+ genericError = function(e) {
+ var key;
+ if (/Mac/i.test(navigator.userAgent)) {
+ key = '&#8984;';
+ } else {
+ key = 'Ctrl';
+ }
+ return showTooltip(e.trigger, "Press " + key + "-C to copy");
+ };
+
+ showTooltip = function(target, title) {
+ return $(target).tooltip({
+ container: 'body',
+ html: 'true',
+ placement: 'auto bottom',
+ title: title,
+ trigger: 'manual'
+ }).tooltip('show').one('mouseleave', function() {
+ return $(this).tooltip('hide');
+ });
+ };
+
+ $(function() {
+ var clipboard;
+
+ clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ clipboard.on('success', genericSuccess);
+ return clipboard.on('error', genericError);
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee
deleted file mode 100644
index 24301e01b10..00000000000
--- a/app/assets/javascripts/copy_to_clipboard.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-#= require clipboard
-
-genericSuccess = (e) ->
- showTooltip(e.trigger, 'Copied!')
-
- # Clear the selection and blur the trigger so it loses its border
- e.clearSelection()
- $(e.trigger).blur()
-
-# Safari doesn't support `execCommand`, so instead we inform the user to
-# copy manually.
-#
-# See http://clipboardjs.com/#browser-support
-genericError = (e) ->
- if /Mac/i.test(navigator.userAgent)
- key = '&#8984;' # Command
- else
- key = 'Ctrl'
-
- showTooltip(e.trigger, "Press #{key}-C to copy")
-
-showTooltip = (target, title) ->
- $(target).
- tooltip(
- container: 'body'
- html: 'true'
- placement: 'auto bottom'
- title: title
- trigger: 'manual'
- ).
- tooltip('show').
- one('mouseleave', -> $(this).tooltip('hide'))
-
-$ ->
- clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]'
- clipboard.on 'success', genericSuccess
- clipboard.on 'error', genericError
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
new file mode 100644
index 00000000000..46d1c3f00c1
--- /dev/null
+++ b/app/assets/javascripts/create_label.js.es6
@@ -0,0 +1,126 @@
+(function (w) {
+ class CreateLabelDropdown {
+ constructor ($el, projectId) {
+ this.$el = $el;
+ this.projectId = projectId;
+ this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
+ this.$cancelButton = $('.js-cancel-label-btn', this.$el);
+ this.$newLabelField = $('#new_label_name', this.$el);
+ this.$newColorField = $('#new_label_color', this.$el);
+ this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
+ this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
+ this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
+
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.disable();
+
+ this.cleanBinding();
+ this.addBinding();
+ }
+
+ cleanBinding () {
+ this.$colorSuggestions.off('click');
+ this.$newLabelField.off('keyup change');
+ this.$newColorField.off('keyup change');
+ this.$dropdownBack.off('click');
+ this.$cancelButton.off('click');
+ this.$newLabelCreateButton.off('click');
+ }
+
+ addBinding () {
+ const self = this;
+
+ this.$colorSuggestions.on('click', function (e) {
+ const $this = $(this);
+ self.addColorValue(e, $this);
+ });
+
+ this.$newLabelField.on('keyup change', this.enableLabelCreateButton.bind(this));
+ this.$newColorField.on('keyup change', this.enableLabelCreateButton.bind(this));
+
+ this.$dropdownBack.on('click', this.resetForm.bind(this));
+
+ this.$cancelButton.on('click', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ self.resetForm();
+ self.$dropdownBack.trigger('click');
+ });
+
+ this.$newLabelCreateButton.on('click', this.saveLabel.bind(this));
+ }
+
+ addColorValue (e, $this) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.$newColorField.val($this.data('color')).trigger('change');
+ this.$colorPreview
+ .css('background-color', $this.data('color'))
+ .parent()
+ .addClass('is-active');
+ }
+
+ enableLabelCreateButton () {
+ if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') {
+ this.$newLabelError.hide();
+ this.$newLabelCreateButton.enable();
+ } else {
+ this.$newLabelCreateButton.disable();
+ }
+ }
+
+ resetForm () {
+ this.$newLabelField
+ .val('')
+ .trigger('change');
+
+ this.$newColorField
+ .val('')
+ .trigger('change');
+
+ this.$colorPreview
+ .css('background-color', '')
+ .parent()
+ .removeClass('is-active');
+ }
+
+ saveLabel (e) {
+ e.preventDefault();
+ e.stopPropagation();
+
+ Api.newLabel(this.projectId, {
+ name: this.$newLabelField.val(),
+ color: this.$newColorField.val()
+ }, (label) => {
+ this.$newLabelCreateButton.enable();
+
+ if (label.message) {
+ let errors;
+
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = label.message.map(function (value, key) {
+ return key + " " + value[0];
+ }).join("<br/>");
+ }
+
+ this.$newLabelError
+ .html(errors)
+ .show();
+ } else {
+ this.$dropdownBack.trigger('click');
+ }
+ });
+ }
+ }
+
+ if (!w.gl) {
+ w.gl = {};
+ }
+
+ gl.CreateLabelDropdown = CreateLabelDropdown;
+})(window);
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
new file mode 100644
index 00000000000..3dd7ceba92f
--- /dev/null
+++ b/app/assets/javascripts/diff.js
@@ -0,0 +1,66 @@
+(function() {
+ this.Diff = (function() {
+ var UNFOLD_COUNT;
+
+ UNFOLD_COUNT = 20;
+
+ function Diff() {
+ $('.files .diff-file').singleFileDiff();
+ this.filesCommentButton = $('.files .diff-file').filesCommentButton();
+ $(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: 1,
+ view: file.data('view')
+ };
+ return $.get(link, params, function(response) {
+ return target.parent().replaceWith(response);
+ });
+ };
+ })(this));
+ }
+
+ 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.coffee b/app/assets/javascripts/diff.js.coffee
deleted file mode 100644
index 6d9b364cb8d..00000000000
--- a/app/assets/javascripts/diff.js.coffee
+++ /dev/null
@@ -1,48 +0,0 @@
-class @Diff
- UNFOLD_COUNT = 20
- constructor: ->
- $(document).off('click', '.js-unfold')
- $(document).on('click', '.js-unfold', (event) =>
- target = $(event.target)
- unfoldBottom = target.hasClass('js-unfold-bottom')
- unfold = true
-
- [old_line, line_number] = @lineNumbers(target.parent())
- offset = line_number - old_line
-
- if unfoldBottom
- line_number += 1
- since = line_number
- to = line_number + UNFOLD_COUNT
- else
- [prev_old_line, prev_new_line] = @lineNumbers(target.parent().prev())
- 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
-
- link = target.parents('.diff-file').attr('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
-
- $.get(link, params, (response) =>
- target.parent().replaceWith(response)
- )
- )
-
- lineNumbers: (line) ->
- return ([0, 0]) unless line.children().length
- lines = line.children().slice(0, 2)
- line_numbers = ($(l).attr('data-linenumber') for l in lines)
- (parseInt(line_number) for line_number in line_numbers)
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
new file mode 100644
index 00000000000..48bc7d77805
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -0,0 +1,49 @@
+((w) => {
+ w.CommentAndResolveBtn = Vue.extend({
+ props: {
+ discussionId: String,
+ textareaIsEmpty: Boolean
+ },
+ computed: {
+ discussion: function () {
+ return CommentsStore.state[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ return this.discussion.isResolved();
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ if (this.textareaIsEmpty) {
+ return "Unresolve discussion";
+ } else {
+ return "Comment & unresolve discussion";
+ }
+ } else {
+ if (this.textareaIsEmpty) {
+ return "Resolve discussion";
+ } else {
+ return "Comment & resolve discussion";
+ }
+ }
+ }
+ },
+ ready: function () {
+ const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
+ this.textareaIsEmpty = $textarea.val() === '';
+
+ $textarea.on('input.comment-and-resolve-btn', () => {
+ this.textareaIsEmpty = $textarea.val() === '';
+ });
+ },
+ destroyed: function () {
+ $(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
+ }
+ });
+})(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
new file mode 100644
index 00000000000..ad80d1118df
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -0,0 +1,188 @@
+(() => {
+ JumpToDiscussion = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ discussionId: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ allResolved: function () {
+ return this.unresolvedDiscussionCount === 0;
+ },
+ showButton: function () {
+ if (this.discussionId) {
+ if (this.unresolvedDiscussionCount > 1) {
+ return true;
+ } else {
+ return this.discussionId !== this.lastResolvedId;
+ }
+ } else {
+ return this.unresolvedDiscussionCount >= 1;
+ }
+ },
+ lastResolvedId: function () {
+ let lastId;
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ lastId = discussion.id;
+ }
+ }
+ return lastId;
+ }
+ },
+ methods: {
+ jumpToNextUnresolvedDiscussion: function () {
+ let discussionsSelector,
+ discussionIdsInScope,
+ firstUnresolvedDiscussionId,
+ nextUnresolvedDiscussionId,
+ activeTab = window.mrTabs.currentAction,
+ hasDiscussionsToJumpTo = true,
+ jumpToFirstDiscussion = !this.discussionId;
+
+ const discussionIdsForElements = function(elements) {
+ return elements.map(function() {
+ return $(this).attr('data-discussion-id');
+ }).toArray();
+ };
+
+ const discussions = this.discussions;
+
+ if (activeTab === 'diffs') {
+ discussionsSelector = '.diffs .notes[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+
+ let unresolvedDiscussionCount = 0;
+
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+ if (discussion && !discussion.isResolved()) {
+ unresolvedDiscussionCount++;
+ }
+ }
+
+ if (this.discussionId && !this.discussion.isResolved()) {
+ // If this is the last unresolved discussion on the diffs tab,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 1) {
+ hasDiscussionsToJumpTo = false;
+ }
+ } else {
+ // If there are no unresolved discussions on the diffs tab at all,
+ // there are no discussions to jump to.
+ if (unresolvedDiscussionCount === 0) {
+ hasDiscussionsToJumpTo = false;
+ }
+ }
+ } else if (activeTab !== 'notes') {
+ // If we are on the commits or builds tabs,
+ // there are no discussions to jump to.
+ hasDiscussionsToJumpTo = false;
+ }
+
+ if (!hasDiscussionsToJumpTo) {
+ // If there are no discussions to jump to on the current page,
+ // switch to the notes tab and jump to the first disucssion there.
+ window.mrTabs.activateTab('notes');
+ activeTab = 'notes';
+ jumpToFirstDiscussion = true;
+ }
+
+ if (activeTab === 'notes') {
+ discussionsSelector = '.discussion[data-discussion-id]';
+ discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
+ }
+
+ let currentDiscussionFound = false;
+ for (let i = 0; i < discussionIdsInScope.length; i++) {
+ const discussionId = discussionIdsInScope[i];
+ const discussion = discussions[discussionId];
+
+ if (!discussion) {
+ // Discussions for comments on commits in this MR don't have a resolved status.
+ continue;
+ }
+
+ if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
+ firstUnresolvedDiscussionId = discussionId;
+
+ if (jumpToFirstDiscussion) {
+ break;
+ }
+ }
+
+ if (!jumpToFirstDiscussion) {
+ if (currentDiscussionFound) {
+ if (!discussion.isResolved()) {
+ nextUnresolvedDiscussionId = discussionId;
+ break;
+ }
+ else {
+ continue;
+ }
+ }
+
+ if (discussionId === this.discussionId) {
+ currentDiscussionFound = true;
+ }
+ }
+ }
+
+ nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
+
+ if (!nextUnresolvedDiscussionId) {
+ return;
+ }
+
+ let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
+
+ if (activeTab === 'notes') {
+ $target = $target.closest('.note-discussion');
+
+ // If the next discussion is closed, toggle it open.
+ if ($target.find('.js-toggle-content').is(':hidden')) {
+ $target.find('.js-toggle-button i').trigger('click')
+ }
+ } else if (activeTab === 'diffs') {
+ // Resolved discussions are hidden in the diffs tab by default.
+ // If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
+ // When jumping between unresolved discussions on the diffs tab, we show them.
+ $target.closest(".content").show();
+
+ $target = $target.closest("tr.notes_holder");
+ $target.show();
+
+ // If we are on the diffs tab, we don't scroll to the discussion itself, but to
+ // 4 diff lines above it: the line the discussion was in response to + 3 context
+ let prevEl;
+ for (let i = 0; i < 4; i++) {
+ prevEl = $target.prev();
+
+ // If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
+ if (!prevEl.hasClass("line_holder")) {
+ break;
+ }
+
+ $target = prevEl;
+ }
+ }
+
+ $.scrollTo($target, {
+ offset: -($('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight())
+ });
+ }
+ }
+ });
+
+ Vue.component('jump-to-discussion', JumpToDiscussion);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
new file mode 100644
index 00000000000..be6ebc77947
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -0,0 +1,107 @@
+((w) => {
+ w.ResolveBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ noteId: Number,
+ discussionId: String,
+ resolved: Boolean,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ resolvedBy: String
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state,
+ loading: false
+ };
+ },
+ watch: {
+ 'discussions': {
+ handler: 'updateTooltip',
+ deep: true
+ }
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ note: function () {
+ if (this.discussion) {
+ return this.discussion.getNote(this.noteId);
+ } else {
+ return undefined;
+ }
+ },
+ buttonText: function () {
+ if (this.isResolved) {
+ return `Resolved by ${this.resolvedByName}`;
+ } else if (this.canResolve) {
+ return 'Mark as resolved';
+ } else {
+ return 'Unable to resolve';
+ }
+ },
+ isResolved: function () {
+ if (this.note) {
+ return this.note.resolved;
+ } else {
+ return false;
+ }
+ },
+ resolvedByName: function () {
+ return this.note.resolved_by;
+ },
+ },
+ methods: {
+ updateTooltip: function () {
+ $(this.$els.button)
+ .tooltip('hide')
+ .tooltip('fixTitle');
+ },
+ resolve: function () {
+ if (!this.canResolve) return;
+
+ let promise;
+ this.loading = true;
+
+ if (this.isResolved) {
+ promise = ResolveService
+ .unresolve(this.namespace, this.noteId);
+ } else {
+ promise = ResolveService
+ .resolve(this.namespace, this.noteId);
+ }
+
+ promise.then((response) => {
+ this.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
+ this.discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
+ }
+
+ this.$nextTick(this.updateTooltip);
+ });
+ }
+ },
+ compiled: function () {
+ $(this.$els.button).tooltip({
+ container: 'body'
+ });
+ },
+ beforeDestroy: function () {
+ CommentsStore.delete(this.discussionId, this.noteId);
+ },
+ created: function () {
+ CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
new file mode 100644
index 00000000000..9e383b14a3e
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -0,0 +1,18 @@
+((w) => {
+ w.ResolveCount = Vue.extend({
+ mixins: [DiscussionMixins],
+ props: {
+ loggedOut: Boolean
+ },
+ data: function () {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ allResolved: function () {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ }
+ }
+ });
+})(window);
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
new file mode 100644
index 00000000000..e373b06b1eb
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -0,0 +1,60 @@
+((w) => {
+ w.ResolveDiscussionBtn = Vue.extend({
+ mixins: [
+ ButtonMixins
+ ],
+ props: {
+ discussionId: String,
+ mergeRequestId: Number,
+ namespacePath: String,
+ projectPath: String,
+ canResolve: Boolean,
+ },
+ data: function() {
+ return {
+ discussions: CommentsStore.state
+ };
+ },
+ computed: {
+ discussion: function () {
+ return this.discussions[this.discussionId];
+ },
+ showButton: function () {
+ if (this.discussion) {
+ return this.discussion.isResolvable();
+ } else {
+ return false;
+ }
+ },
+ isDiscussionResolved: function () {
+ if (this.discussion) {
+ return this.discussion.isResolved();
+ } else {
+ return false;
+ }
+ },
+ buttonText: function () {
+ if (this.isDiscussionResolved) {
+ return "Unresolve discussion";
+ } else {
+ return "Resolve discussion";
+ }
+ },
+ loading: function () {
+ if (this.discussion) {
+ return this.discussion.loading;
+ } else {
+ return false;
+ }
+ }
+ },
+ methods: {
+ resolve: function () {
+ ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ }
+ },
+ created: function () {
+ CommentsStore.createDiscussion(this.discussionId, this.canResolve);
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
new file mode 100644
index 00000000000..22d9cf6c857
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -0,0 +1,35 @@
+//= require vue
+//= require vue-resource
+//= require_directory ./models
+//= require_directory ./stores
+//= require_directory ./services
+//= require_directory ./mixins
+//= 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));
+ });
+ }
+ }
+ }
+ });
+
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ }
+ });
+});
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
new file mode 100644
index 00000000000..a05f885201d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -0,0 +1,35 @@
+((w) => {
+ w.DiscussionMixins = {
+ computed: {
+ discussionCount: function () {
+ return Object.keys(this.discussions).length;
+ },
+ resolvedDiscussionCount: function () {
+ let resolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (discussion.isResolved()) {
+ resolvedCount++;
+ }
+ }
+
+ return resolvedCount;
+ },
+ unresolvedDiscussionCount: function () {
+ let unresolvedCount = 0;
+
+ for (const discussionId in this.discussions) {
+ const discussion = this.discussions[discussionId];
+
+ if (!discussion.isResolved()) {
+ unresolvedCount++;
+ }
+ }
+
+ return unresolvedCount;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
new file mode 100644
index 00000000000..d278678085b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
@@ -0,0 +1,9 @@
+((w) => {
+ w.ButtonMixins = {
+ computed: {
+ namespace: function () {
+ return `${this.namespacePath}/${this.projectPath}`;
+ }
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
new file mode 100644
index 00000000000..488714e4870
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -0,0 +1,87 @@
+class DiscussionModel {
+ constructor (discussionId) {
+ this.id = discussionId;
+ this.notes = {};
+ this.loading = false;
+ this.canResolve = false;
+ }
+
+ createNote (noteId, canResolve, resolved, resolved_by) {
+ Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+ }
+
+ deleteNote (noteId) {
+ Vue.delete(this.notes, noteId);
+ }
+
+ getNote (noteId) {
+ return this.notes[noteId];
+ }
+
+ notesCount() {
+ return Object.keys(this.notes).length;
+ }
+
+ isResolved () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ resolveAllNotes (resolved_by) {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (!note.resolved) {
+ note.resolved = true;
+ note.resolved_by = resolved_by;
+ }
+ }
+ }
+
+ unResolveAllNotes () {
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.resolved) {
+ note.resolved = false;
+ note.resolved_by = null;
+ }
+ }
+ }
+
+ updateHeadline (data) {
+ const $discussionHeadline = $(`.discussion[data-discussion-id="${this.id}"] .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);
+ }
+ } else {
+ $discussionHeadline.remove();
+ }
+ }
+
+ isResolvable () {
+ if (!this.canResolve) {
+ return false;
+ }
+
+ for (const noteId in this.notes) {
+ const note = this.notes[noteId];
+
+ if (note.canResolve) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
new file mode 100644
index 00000000000..f2d2d389c38
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -0,0 +1,9 @@
+class NoteModel {
+ constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+ this.discussionId = discussionId;
+ this.id = noteId;
+ this.canResolve = canResolve;
+ this.resolved = resolved;
+ this.resolved_by = resolved_by;
+ }
+}
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
new file mode 100644
index 00000000000..de771ff814b
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -0,0 +1,88 @@
+((w) => {
+ class ResolveServiceClass {
+ constructor() {
+ this.noteResource = Vue.resource('notes{/noteId}/resolve');
+ this.discussionResource = Vue.resource('merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve');
+ }
+
+ setCSRF() {
+ Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
+ }
+
+ prepareRequest(namespace) {
+ this.setCSRF();
+ Vue.http.options.root = `/${namespace}`;
+ }
+
+ resolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.save({ noteId }, {});
+ }
+
+ unresolve(namespace, noteId) {
+ this.prepareRequest(namespace);
+
+ return this.noteResource.delete({ noteId }, {});
+ }
+
+ toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId],
+ isResolved = discussion.isResolved();
+ let promise;
+
+ if (isResolved) {
+ promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ } else {
+ promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ }
+
+ promise.then((response) => {
+ discussion.loading = false;
+
+ if (response.status === 200) {
+ const data = response.json();
+ const resolved_by = data ? data.resolved_by : null;
+
+ if (isResolved) {
+ discussion.unResolveAllNotes();
+ } else {
+ discussion.resolveAllNotes(resolved_by);
+ }
+
+ discussion.updateHeadline(data);
+ } else {
+ new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
+ }
+ })
+ }
+
+ resolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.save({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+
+ unResolveAll(namespace, mergeRequestId, discussionId) {
+ const discussion = CommentsStore.state[discussionId];
+
+ this.prepareRequest(namespace);
+
+ discussion.loading = true;
+
+ return this.discussionResource.delete({
+ mergeRequestId,
+ discussionId
+ }, {});
+ }
+ }
+
+ w.ResolveService = new ResolveServiceClass();
+})(window);
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
new file mode 100644
index 00000000000..69522e1dac5
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -0,0 +1,53 @@
+((w) => {
+ w.CommentsStore = {
+ state: {},
+ get: function (discussionId, noteId) {
+ return this.state[discussionId].getNote(noteId);
+ },
+ createDiscussion: function (discussionId, canResolve) {
+ let discussion = this.state[discussionId];
+ if (!this.state[discussionId]) {
+ discussion = new DiscussionModel(discussionId);
+ Vue.set(this.state, discussionId, discussion);
+ }
+
+ if (canResolve !== undefined) {
+ discussion.canResolve = canResolve;
+ }
+
+ return discussion;
+ },
+ create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
+ const discussion = this.createDiscussion(discussionId);
+
+ discussion.createNote(noteId, canResolve, resolved, resolved_by);
+ },
+ update: function (discussionId, noteId, resolved, resolved_by) {
+ const discussion = this.state[discussionId];
+ const note = discussion.getNote(noteId);
+ note.resolved = resolved;
+ note.resolved_by = resolved_by;
+ },
+ delete: function (discussionId, noteId) {
+ const discussion = this.state[discussionId];
+ discussion.deleteNote(noteId);
+
+ if (discussion.notesCount() === 0) {
+ Vue.delete(this.state, discussionId);
+ }
+ },
+ unresolvedDiscussionIds: function () {
+ let ids = [];
+
+ for (const discussionId in this.state) {
+ const discussion = this.state[discussionId];
+
+ if (!discussion.isResolved()) {
+ ids.push(discussion.id);
+ }
+ }
+
+ return ids;
+ }
+ };
+})(window);
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
new file mode 100644
index 00000000000..ba64d2bcf0b
--- /dev/null
+++ b/app/assets/javascripts/dispatcher.js
@@ -0,0 +1,280 @@
+(function() {
+ var Dispatcher;
+
+ $(function() {
+ return new Dispatcher();
+ });
+
+ Dispatcher = (function() {
+ function Dispatcher() {
+ this.initSearch();
+ this.initPageScripts();
+ }
+
+ Dispatcher.prototype.initPageScripts = function() {
+ var page, path, shortcut_handler;
+ page = $('body').attr('data-page');
+ if (!page) {
+ return false;
+ }
+ path = page.split(':');
+ shortcut_handler = null;
+ switch (page) {
+ case 'projects:boards:show':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:issues:index':
+ Issuable.init();
+ new IssuableBulkActions();
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:issues:show':
+ new Issue();
+ shortcut_handler = new ShortcutsIssuable();
+ new ZenMode();
+ break;
+ case 'projects:milestones:show':
+ case 'groups:milestones:show':
+ case 'dashboard:milestones:show':
+ new Milestone();
+ break;
+ case 'dashboard:todos:index':
+ new Todos();
+ break;
+ case 'projects:milestones:new':
+ case 'projects:milestones:edit':
+ new ZenMode();
+ new DueDateSelect();
+ new GLForm($('.milestone-form'));
+ break;
+ case 'groups:milestones:new':
+ new ZenMode();
+ break;
+ case 'projects:compare:show':
+ new Diff();
+ break;
+ case 'projects:issues:new':
+ case 'projects:issues:edit':
+ shortcut_handler = new ShortcutsNavigation();
+ new GLForm($('.issue-form'));
+ new IssuableForm($('.issue-form'));
+ new IssuableTemplateSelectors();
+ break;
+ case 'projects:merge_requests:new':
+ case 'projects:merge_requests:edit':
+ new Diff();
+ shortcut_handler = new ShortcutsNavigation();
+ new GLForm($('.merge-request-form'));
+ new IssuableForm($('.merge-request-form'));
+ new IssuableTemplateSelectors();
+ break;
+ case 'projects:tags:new':
+ new ZenMode();
+ new GLForm($('.tag-form'));
+ break;
+ case 'projects:releases:edit':
+ new ZenMode();
+ new GLForm($('.release-form'));
+ break;
+ case 'projects:merge_requests:show':
+ new Diff();
+ shortcut_handler = new ShortcutsIssuable(true);
+ new ZenMode();
+ new MergedButtons();
+ break;
+ case 'projects:merge_requests:commits':
+ case 'projects:merge_requests:builds':
+ new MergedButtons();
+ break;
+ case "projects:merge_requests:diffs":
+ new Diff();
+ new ZenMode();
+ new MergedButtons();
+ break;
+ case "projects:merge_requests:conflicts":
+ window.mcui = new MergeConflictResolver()
+ case 'projects:merge_requests:index':
+ shortcut_handler = new ShortcutsNavigation();
+ Issuable.init();
+ break;
+ case 'dashboard:activity':
+ new Activities();
+ break;
+ case 'dashboard:projects:starred':
+ new Activities();
+ break;
+ case 'projects:commit:show':
+ new Commit();
+ new Diff();
+ new ZenMode();
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:commits:show':
+ case 'projects:activity':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
+ case 'projects:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new NotificationsForm();
+ if ($('#tree-slider').length) {
+ new TreeView();
+ }
+ break;
+ case 'groups:activity':
+ new Activities();
+ break;
+ case 'groups:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new NotificationsForm();
+ new NotificationsDropdown();
+ break;
+ case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
+ new GroupMembers();
+ new UsersSelect();
+ break;
+ case 'projects:project_members:index':
+ new gl.MemberExpirationDate();
+ new ProjectMembers();
+ new UsersSelect();
+ break;
+ case 'groups:new':
+ case 'groups:edit':
+ case 'admin:groups:edit':
+ case 'admin:groups:new':
+ new GroupAvatar();
+ break;
+ case 'projects:tree:show':
+ shortcut_handler = new ShortcutsNavigation();
+ new TreeView();
+ break;
+ case 'projects:find_file:show':
+ shortcut_handler = true;
+ break;
+ case 'projects:blob:show':
+ case 'projects:blame:show':
+ new LineHighlighter();
+ shortcut_handler = new ShortcutsNavigation();
+ new ShortcutsBlob(true);
+ break;
+ case 'projects:labels:new':
+ case 'projects:labels:edit':
+ new Labels();
+ break;
+ case 'projects:labels:index':
+ if ($('.prioritized-labels').length) {
+ new LabelManager();
+ }
+ break;
+ case 'projects:network:show':
+ shortcut_handler = true;
+ break;
+ case 'projects:forks:new':
+ new ProjectFork();
+ break;
+ case 'projects:artifacts:browse':
+ new BuildArtifacts();
+ break;
+ case 'projects:group_links:index':
+ new gl.MemberExpirationDate();
+ new GroupsSelect();
+ break;
+ case 'search:show':
+ new Search();
+ break;
+ case 'projects:protected_branches:index':
+ new gl.ProtectedBranchCreate();
+ new gl.ProtectedBranchEditList();
+ break;
+ }
+ switch (path.first()) {
+ case 'admin':
+ new Admin();
+ switch (path[1]) {
+ case 'groups':
+ new UsersSelect();
+ break;
+ case 'projects':
+ new NamespaceSelects();
+ break;
+ case 'labels':
+ switch (path[2]) {
+ case 'edit':
+ new Labels();
+ }
+ case 'abuse_reports':
+ new gl.AbuseReports();
+ break;
+ }
+ break;
+ case 'dashboard':
+ case 'root':
+ shortcut_handler = new ShortcutsDashboardNavigation();
+ break;
+ case 'profiles':
+ new NotificationsForm();
+ new NotificationsDropdown();
+ break;
+ case 'projects':
+ new Project();
+ new ProjectAvatar();
+ switch (path[1]) {
+ case 'compare':
+ new CompareAutocomplete();
+ break;
+ case 'edit':
+ shortcut_handler = new ShortcutsNavigation();
+ new ProjectNew();
+ break;
+ case 'new':
+ new ProjectNew();
+ break;
+ case 'show':
+ new Star();
+ new ProjectNew();
+ new ProjectShow();
+ new NotificationsDropdown();
+ break;
+ case 'wikis':
+ new Wikis();
+ shortcut_handler = new ShortcutsNavigation();
+ new ZenMode();
+ new GLForm($('.wiki-form'));
+ break;
+ case 'snippets':
+ shortcut_handler = new ShortcutsNavigation();
+ if (path[2] === 'show') {
+ new ZenMode();
+ }
+ break;
+ case 'labels':
+ case 'graphs':
+ case 'compare':
+ case 'pipelines':
+ case 'forks':
+ case 'milestones':
+ case 'project_members':
+ case 'deploy_keys':
+ case 'builds':
+ case 'hooks':
+ case 'services':
+ case 'protected_branches':
+ shortcut_handler = new ShortcutsNavigation();
+ }
+ }
+ if (!shortcut_handler) {
+ return new Shortcuts();
+ }
+ };
+
+ Dispatcher.prototype.initSearch = function() {
+ if ($('.search').length) {
+ return new SearchAutocomplete();
+ }
+ };
+
+ return Dispatcher;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
deleted file mode 100644
index 74fd77cf7ab..00000000000
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ /dev/null
@@ -1,169 +0,0 @@
-$ ->
- new Dispatcher()
-
-class Dispatcher
- constructor: () ->
- @initSearch()
- @initPageScripts()
-
- initPageScripts: ->
- page = $('body').attr('data-page')
-
- unless page
- return false
-
- path = page.split(':')
- shortcut_handler = null
- switch page
- when 'projects:issues:index'
- Issuable.init()
- new IssuableBulkActions()
- shortcut_handler = new ShortcutsNavigation()
- when 'projects:issues:show'
- new Issue()
- shortcut_handler = new ShortcutsIssuable()
- new ZenMode()
- when 'projects:milestones:show', 'groups:milestones:show', 'dashboard:milestones:show'
- new Milestone()
- when 'dashboard:todos:index'
- new Todos()
- when 'projects:milestones:new', 'projects:milestones:edit'
- new ZenMode()
- new DueDateSelect()
- new GLForm($('.milestone-form'))
- when 'groups:milestones:new'
- new ZenMode()
- when 'projects:compare:show'
- new Diff()
- when 'projects:issues:new','projects:issues:edit'
- shortcut_handler = new ShortcutsNavigation()
- new GLForm($('.issue-form'))
- new IssuableForm($('.issue-form'))
- when 'projects:merge_requests:new', 'projects:merge_requests:edit'
- new Diff()
- shortcut_handler = new ShortcutsNavigation()
- new GLForm($('.merge-request-form'))
- new IssuableForm($('.merge-request-form'))
- when 'projects:tags:new'
- new ZenMode()
- new GLForm($('.tag-form'))
- when 'projects:releases:edit'
- new ZenMode()
- new GLForm($('.release-form'))
- when 'projects:merge_requests:show'
- new Diff()
- shortcut_handler = new ShortcutsIssuable(true)
- new ZenMode()
- new MergedButtons()
- when 'projects:merge_requests:commits', 'projects:merge_requests:builds'
- new MergedButtons()
- when "projects:merge_requests:diffs"
- new Diff()
- new ZenMode()
- new MergedButtons()
- when 'projects:merge_requests:index'
- shortcut_handler = new ShortcutsNavigation()
- Issuable.init()
- when 'dashboard:activity'
- new Activities()
- when 'dashboard:projects:starred'
- new Activities()
- when 'projects:commit:show'
- new Commit()
- new Diff()
- new ZenMode()
- shortcut_handler = new ShortcutsNavigation()
- when 'projects:commits:show', 'projects:activity'
- shortcut_handler = new ShortcutsNavigation()
- when 'projects:show'
- shortcut_handler = new ShortcutsNavigation()
-
- new NotificationsForm()
- new TreeView() if $('#tree-slider').length
- when 'groups:activity'
- new Activities()
- when 'groups:show'
- shortcut_handler = new ShortcutsNavigation()
- new NotificationsForm()
- new NotificationsDropdown()
- when 'groups:group_members:index'
- new GroupMembers()
- new UsersSelect()
- when 'projects:project_members:index'
- new ProjectMembers()
- new UsersSelect()
- when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
- new GroupAvatar()
- when 'projects:tree:show'
- shortcut_handler = new ShortcutsNavigation()
- new TreeView()
- when 'projects:find_file:show'
- shortcut_handler = true
- when 'projects:blob:show', 'projects:blame:show'
- new LineHighlighter()
- shortcut_handler = new ShortcutsNavigation()
- new ShortcutsBlob true
- when 'projects:labels:new', 'projects:labels:edit'
- new Labels()
- when 'projects:labels:index'
- new LabelManager() if $('.prioritized-labels').length
- when 'projects:network:show'
- # Ensure we don't create a particular shortcut handler here. This is
- # already created, where the network graph is created.
- shortcut_handler = true
- when 'projects:forks:new'
- new ProjectFork()
- when 'projects:artifacts:browse'
- new BuildArtifacts()
- when 'projects:group_links:index'
- new GroupsSelect()
- when 'search:show'
- new Search()
-
- switch path.first()
- when 'admin'
- new Admin()
- switch path[1]
- when 'groups'
- new UsersSelect()
- when 'projects'
- new NamespaceSelects()
- when 'dashboard', 'root'
- shortcut_handler = new ShortcutsDashboardNavigation()
- when 'profiles'
- new NotificationsForm()
- new NotificationsDropdown()
- when 'projects'
- new Project()
- new ProjectAvatar()
- switch path[1]
- when 'edit'
- shortcut_handler = new ShortcutsNavigation()
- new ProjectNew()
- when 'new'
- new ProjectNew()
- when 'show'
- new ProjectNew()
- new ProjectShow()
- new NotificationsDropdown()
- when 'wikis'
- new Wikis()
- shortcut_handler = new ShortcutsNavigation()
- new ZenMode()
- new GLForm($('.wiki-form'))
- when 'snippets'
- shortcut_handler = new ShortcutsNavigation()
- new ZenMode() if path[2] == 'show'
- when 'labels', 'graphs', 'compare', 'pipelines', 'forks', \
- 'milestones', 'project_members', 'deploy_keys', 'builds', \
- 'hooks', 'services', 'protected_branches'
- shortcut_handler = new ShortcutsNavigation()
-
- # If we haven't installed a custom shortcut handler, install the default one
- if not shortcut_handler
- new Shortcuts()
-
- initSearch: ->
-
- # Only when search form is present
- new SearchAutocomplete() if $('.search').length
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
new file mode 100644
index 00000000000..4a6fea929c7
--- /dev/null
+++ b/app/assets/javascripts/dropzone_input.js
@@ -0,0 +1,219 @@
+
+/*= require preview_markdown */
+
+(function() {
+ this.DropzoneInput = (function() {
+ function DropzoneInput(form) {
+ var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, project_uploads_path, showError, showSpinner, uploadFile, uploadProgress;
+ Dropzone.autoDiscover = false;
+ alertClass = "alert alert-danger alert-dismissable div-dropzone-alert";
+ alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\"";
+ divHover = "<div class=\"div-dropzone-hover\"></div>";
+ divSpinner = "<div class=\"div-dropzone-spinner\"></div>";
+ divAlert = "<div class=\"" + alertClass + "\"></div>";
+ iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>";
+ iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>";
+ uploadProgress = $("<div class=\"div-dropzone-progress\"></div>");
+ btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>";
+ project_uploads_path = window.project_uploads_path || null;
+ max_file_size = gon.max_file_size || 10;
+ form_textarea = $(form).find(".js-gfm-input");
+ form_textarea.wrap("<div class=\"div-dropzone\"></div>");
+ form_textarea.on('paste', (function(_this) {
+ return function(event) {
+ return handlePaste(event);
+ };
+ })(this));
+ $mdArea = $(form_textarea).closest('.md-area');
+ $(form).setupMarkdownPreview();
+ form_dropzone = $(form).find('.div-dropzone');
+ form_dropzone.parent().addClass("div-dropzone-wrapper");
+ form_dropzone.append(divHover);
+ form_dropzone.find(".div-dropzone-hover").append(iconPaperclip);
+ form_dropzone.append(divSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(iconSpinner);
+ form_dropzone.find(".div-dropzone-spinner").append(uploadProgress);
+ form_dropzone.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
+ });
+ dropzone = form_dropzone.dropzone({
+ url: project_uploads_path,
+ dictDefaultMessage: "",
+ clickable: true,
+ paramName: "file",
+ maxFilesize: max_file_size,
+ uploadMultiple: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ previewContainer: false,
+ processing: function() {
+ return $(".div-dropzone-alert").alert("close");
+ },
+ dragover: function() {
+ $mdArea.addClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0.7);
+ },
+ dragleave: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ },
+ drop: function() {
+ $mdArea.removeClass('is-dropzone-hover');
+ form.find(".div-dropzone-hover").css("opacity", 0);
+ form_textarea.focus();
+ },
+ success: function(header, response) {
+ pasteText(response.link.markdown);
+ },
+ error: function(temp) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed.");
+ }
+ },
+ totaluploadprogress: function(totalUploadProgress) {
+ uploadProgress.text(Math.round(totalUploadProgress) + "%");
+ },
+ sending: function() {
+ form_dropzone.find(".div-dropzone-spinner").css({
+ "opacity": 0.7,
+ "display": "inherit"
+ });
+ },
+ queuecomplete: function() {
+ uploadProgress.text("");
+ $(".dz-preview").remove();
+ $(".markdown-area").trigger("input");
+ $(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
+ });
+ }
+ });
+ child = $(dropzone[0]).children("textarea");
+ handlePaste = function(event) {
+ var filename, image, pasteEvent, text;
+ pasteEvent = event.originalEvent;
+ if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
+ image = isImage(pasteEvent);
+ if (image) {
+ event.preventDefault();
+ filename = getFilename(pasteEvent) || "image.png";
+ text = "{{" + filename + "}}";
+ pasteText(text);
+ return uploadFile(image.getAsFile(), filename);
+ }
+ }
+ };
+ isImage = function(data) {
+ var i, item;
+ i = 0;
+ while (i < data.clipboardData.items.length) {
+ item = data.clipboardData.items[i];
+ if (item.type.indexOf("image") !== -1) {
+ return item;
+ }
+ i++;
+ }
+ return false;
+ };
+ pasteText = function(text) {
+ var afterSelection, beforeSelection, caretEnd, caretStart, textEnd;
+ caretStart = $(child)[0].selectionStart;
+ caretEnd = $(child)[0].selectionEnd;
+ textEnd = $(child).val().length;
+ beforeSelection = $(child).val().substring(0, caretStart);
+ afterSelection = $(child).val().substring(caretEnd, textEnd);
+ $(child).val(beforeSelection + text + afterSelection);
+ child.get(0).setSelectionRange(caretStart + text.length, caretEnd + text.length);
+ return form_textarea.trigger("input");
+ };
+ getFilename = function(e) {
+ var value;
+ if (window.clipboardData && window.clipboardData.getData) {
+ value = window.clipboardData.getData("Text");
+ } else if (e.clipboardData && e.clipboardData.getData) {
+ value = e.clipboardData.getData("text/plain");
+ }
+ value = value.split("\r");
+ return value.first();
+ };
+ uploadFile = function(item, filename) {
+ var formData;
+ formData = new FormData();
+ formData.append("file", item, filename);
+ return $.ajax({
+ url: project_uploads_path,
+ type: "POST",
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ headers: {
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+ },
+ beforeSend: function() {
+ showSpinner();
+ return closeAlertMessage();
+ },
+ success: function(e, textStatus, response) {
+ return insertToTextArea(filename, response.responseJSON.link.markdown);
+ },
+ error: function(response) {
+ return showError(response.responseJSON.message);
+ },
+ complete: function() {
+ return closeSpinner();
+ }
+ });
+ };
+ insertToTextArea = function(filename, url) {
+ return $(child).val(function(index, val) {
+ return val.replace("{{" + filename + "}}", url + "\n");
+ });
+ };
+ appendToTextArea = function(url) {
+ return $(child).val(function(index, val) {
+ return val + url + "\n";
+ });
+ };
+ showSpinner = function(e) {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0.7,
+ "display": "inherit"
+ });
+ };
+ closeSpinner = function() {
+ return form.find(".div-dropzone-spinner").css({
+ "opacity": 0,
+ "display": "none"
+ });
+ };
+ showError = function(message) {
+ var checkIfMsgExists, errorAlert;
+ errorAlert = $(form).find('.error-alert');
+ checkIfMsgExists = errorAlert.children().length;
+ if (checkIfMsgExists === 0) {
+ errorAlert.append(divAlert);
+ return $(".div-dropzone-alert").append(btnAlert + message);
+ }
+ };
+ closeAlertMessage = function() {
+ return form.find(".div-dropzone-alert").alert("close");
+ };
+ form.find(".markdown-selector").click(function(e) {
+ e.preventDefault();
+ $(this).closest('.gfm-form').find('.div-dropzone').click();
+ });
+ }
+
+ return DropzoneInput;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
deleted file mode 100644
index 665246e2a7d..00000000000
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ /dev/null
@@ -1,201 +0,0 @@
-#= require markdown_preview
-
-class @DropzoneInput
- constructor: (form) ->
- Dropzone.autoDiscover = false
- alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"
- alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""
- divHover = "<div class=\"div-dropzone-hover\"></div>"
- divSpinner = "<div class=\"div-dropzone-spinner\"></div>"
- divAlert = "<div class=\"" + alertClass + "\"></div>"
- iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"
- iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"
- uploadProgress = $("<div class=\"div-dropzone-progress\"></div>")
- btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"
- project_uploads_path = window.project_uploads_path or null
- max_file_size = gon.max_file_size or 10
-
- form_textarea = $(form).find(".js-gfm-input")
- form_textarea.wrap "<div class=\"div-dropzone\"></div>"
- form_textarea.on 'paste', (event) =>
- handlePaste(event)
-
- $mdArea = $(form_textarea).closest('.md-area')
-
- $(form).setupMarkdownPreview()
-
- form_dropzone = $(form).find('.div-dropzone')
- form_dropzone.parent().addClass "div-dropzone-wrapper"
- form_dropzone.append divHover
- form_dropzone.find(".div-dropzone-hover").append iconPaperclip
- form_dropzone.append divSpinner
- form_dropzone.find(".div-dropzone-spinner").append iconSpinner
- form_dropzone.find(".div-dropzone-spinner").append uploadProgress
- form_dropzone.find(".div-dropzone-spinner").css
- "opacity": 0
- "display": "none"
-
- dropzone = form_dropzone.dropzone(
- url: project_uploads_path
- dictDefaultMessage: ""
- clickable: true
- paramName: "file"
- maxFilesize: max_file_size
- uploadMultiple: false
- headers:
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
-
- previewContainer: false
-
- processing: ->
- $(".div-dropzone-alert").alert "close"
-
- dragover: ->
- $mdArea.addClass 'is-dropzone-hover'
- form.find(".div-dropzone-hover").css "opacity", 0.7
- return
-
- dragleave: ->
- $mdArea.removeClass 'is-dropzone-hover'
- form.find(".div-dropzone-hover").css "opacity", 0
- return
-
- drop: ->
- $mdArea.removeClass 'is-dropzone-hover'
- form.find(".div-dropzone-hover").css "opacity", 0
- form_textarea.focus()
- return
-
- success: (header, response) ->
- pasteText response.link.markdown
- return
-
- error: (temp) ->
- errorAlert = $(form).find('.error-alert')
- checkIfMsgExists = errorAlert.children().length
- if checkIfMsgExists is 0
- errorAlert.append divAlert
- $(".div-dropzone-alert").append "#{btnAlert}Attaching the file failed."
- return
-
- totaluploadprogress: (totalUploadProgress) ->
- uploadProgress.text Math.round(totalUploadProgress) + "%"
- return
-
- sending: ->
- form_dropzone.find(".div-dropzone-spinner").css
- "opacity": 0.7
- "display": "inherit"
- return
-
- queuecomplete: ->
- uploadProgress.text ""
- $(".dz-preview").remove()
- $(".markdown-area").trigger "input"
- $(".div-dropzone-spinner").css
- "opacity": 0
- "display": "none"
- return
- )
-
- child = $(dropzone[0]).children("textarea")
-
- handlePaste = (event) ->
- pasteEvent = event.originalEvent
- if pasteEvent.clipboardData and pasteEvent.clipboardData.items
- image = isImage(pasteEvent)
- if image
- event.preventDefault()
-
- filename = getFilename(pasteEvent) or "image.png"
- text = "{{" + filename + "}}"
- pasteText(text)
- uploadFile image.getAsFile(), filename
-
- isImage = (data) ->
- i = 0
- while i < data.clipboardData.items.length
- item = data.clipboardData.items[i]
- if item.type.indexOf("image") isnt -1
- return item
- i++
- return false
-
- pasteText = (text) ->
- caretStart = $(child)[0].selectionStart
- caretEnd = $(child)[0].selectionEnd
- textEnd = $(child).val().length
-
- beforeSelection = $(child).val().substring 0, caretStart
- afterSelection = $(child).val().substring caretEnd, textEnd
- $(child).val beforeSelection + text + afterSelection
- child.get(0).setSelectionRange caretStart + text.length, caretEnd + text.length
- form_textarea.trigger "input"
-
- getFilename = (e) ->
- if window.clipboardData and window.clipboardData.getData
- value = window.clipboardData.getData("Text")
- else if e.clipboardData and e.clipboardData.getData
- value = e.clipboardData.getData("text/plain")
-
- value = value.split("\r")
- value.first()
-
- uploadFile = (item, filename) ->
- formData = new FormData()
- formData.append "file", item, filename
- $.ajax
- url: project_uploads_path
- type: "POST"
- data: formData
- dataType: "json"
- processData: false
- contentType: false
- headers:
- "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
-
- beforeSend: ->
- showSpinner()
- closeAlertMessage()
-
- success: (e, textStatus, response) ->
- insertToTextArea(filename, response.responseJSON.link.markdown)
-
- error: (response) ->
- showError(response.responseJSON.message)
-
- complete: ->
- closeSpinner()
-
- insertToTextArea = (filename, url) ->
- $(child).val (index, val) ->
- val.replace("{{" + filename + "}}", url + "\n")
-
- appendToTextArea = (url) ->
- $(child).val (index, val) ->
- val + url + "\n"
-
- showSpinner = (e) ->
- form.find(".div-dropzone-spinner").css
- "opacity": 0.7
- "display": "inherit"
-
- closeSpinner = ->
- form.find(".div-dropzone-spinner").css
- "opacity": 0
- "display": "none"
-
- showError = (message) ->
- errorAlert = $(form).find('.error-alert')
- checkIfMsgExists = errorAlert.children().length
- if checkIfMsgExists is 0
- errorAlert.append divAlert
- $(".div-dropzone-alert").append btnAlert + message
-
- closeAlertMessage = ->
- form.find(".div-dropzone-alert").alert "close"
-
- form.find(".markdown-selector").click (e) ->
- e.preventDefault()
- $(@).closest('.gfm-form').find('.div-dropzone').click()
- return
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
new file mode 100644
index 00000000000..5a725a41fd1
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js
@@ -0,0 +1,104 @@
+(function() {
+ this.DueDateSelect = (function() {
+ function DueDateSelect() {
+ var $datePicker, $dueDate, $loading;
+ $datePicker = $('.datepicker');
+ if ($datePicker.length) {
+ $dueDate = $('#milestone_due_date');
+ $datePicker.datepicker({
+ dateFormat: 'yy-mm-dd',
+ onSelect: function(dateText, inst) {
+ return $dueDate.val(dateText);
+ }
+ }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
+ }
+ $('.js-clear-due-date').on('click', function(e) {
+ e.preventDefault();
+ return $.datepicker._clearDate($datePicker);
+ });
+ $loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
+ $('.js-due-date-select').each(function(i, dropdown) {
+ var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
+ $dropdown = $(dropdown);
+ $dropdownParent = $dropdown.closest('.dropdown');
+ $datePicker = $dropdownParent.find('.js-due-date-calendar');
+ $block = $dropdown.closest('.block');
+ $selectbox = $dropdown.closest('.selectbox');
+ $value = $block.find('.value');
+ $valueContent = $block.find('.value-content');
+ $sidebarValue = $('.js-due-date-sidebar-value', $block);
+ fieldName = $dropdown.data('field-name');
+ abilityName = $dropdown.data('ability-name');
+ issueUpdateURL = $dropdown.data('issue-update');
+ $dropdown.glDropdown({
+ hidden: function() {
+ $selectbox.hide();
+ return $value.css('display', '');
+ }
+ });
+ addDueDate = function(isDropdown) {
+ var data, date, mediumDate, value;
+ value = $("input[name='" + fieldName + "']").val();
+ if (value !== '') {
+ date = new Date(value.replace(new RegExp('-', 'g'), ','));
+ mediumDate = $.datepicker.formatDate('M d, yy', date);
+ } else {
+ mediumDate = 'No due date';
+ }
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].due_date = value;
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ data: data,
+ dataType: 'json',
+ beforeSend: function() {
+ var cssClass;
+ $loading.fadeIn();
+ if (isDropdown) {
+ $dropdown.trigger('loading.gl.dropdown');
+ $selectbox.hide();
+ }
+ $value.css('display', '');
+ cssClass = Date.parse(mediumDate) ? 'bold' : 'no-value';
+ $valueContent.html("<span class='" + cssClass + "'>" + mediumDate + "</span>");
+ $sidebarValue.html(mediumDate);
+ if (value !== '') {
+ return $('.js-remove-due-date-holder').removeClass('hidden');
+ } else {
+ return $('.js-remove-due-date-holder').addClass('hidden');
+ }
+ }
+ }).done(function(data) {
+ if (isDropdown) {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $dropdown.dropdown('toggle');
+ }
+ return $loading.fadeOut();
+ });
+ };
+ $block.on('click', '.js-remove-due-date', function(e) {
+ e.preventDefault();
+ $("input[name='" + fieldName + "']").val('');
+ return addDueDate(false);
+ });
+ return $datePicker.datepicker({
+ dateFormat: 'yy-mm-dd',
+ defaultDate: $("input[name='" + fieldName + "']").val(),
+ altField: "input[name='" + fieldName + "']",
+ onSelect: function() {
+ return addDueDate(true);
+ }
+ });
+ });
+ $(document).off('click', '.ui-datepicker-header a').on('click', '.ui-datepicker-header a', function(e) {
+ return e.stopImmediatePropagation();
+ });
+ }
+
+ return DueDateSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
deleted file mode 100644
index d65c018dad5..00000000000
--- a/app/assets/javascripts/due_date_select.js.coffee
+++ /dev/null
@@ -1,99 +0,0 @@
-class @DueDateSelect
- constructor: ->
- # Milestone edit/new form
- $datePicker = $('.datepicker')
-
- if $datePicker.length
- $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) ->
- e.preventDefault()
- $.datepicker._clearDate($datePicker)
-
- # Issuable sidebar
- $loading = $('.js-issuable-update .due_date')
- .find('.block-loading')
- .hide()
-
- $('.js-due-date-select').each (i, dropdown) ->
- $dropdown = $(dropdown)
- $dropdownParent = $dropdown.closest('.dropdown')
- $datePicker = $dropdownParent.find('.js-due-date-calendar')
- $block = $dropdown.closest('.block')
- $selectbox = $dropdown.closest('.selectbox')
- $value = $block.find('.value')
- $valueContent = $block.find('.value-content')
- $sidebarValue = $('.js-due-date-sidebar-value', $block)
-
- fieldName = $dropdown.data('field-name')
- abilityName = $dropdown.data('ability-name')
- issueUpdateURL = $dropdown.data('issue-update')
-
- $dropdown.glDropdown(
- hidden: ->
- $selectbox.hide()
- $value.css('display', '')
- )
-
- addDueDate = (isDropdown) ->
- # Create the post date
- value = $("input[name='#{fieldName}']").val()
-
- if value isnt ''
- date = new Date value.replace(new RegExp('-', 'g'), ',')
- mediumDate = $.datepicker.formatDate 'M d, yy', date
- else
- mediumDate = 'No due date'
-
- data = {}
- data[abilityName] = {}
- data[abilityName].due_date = value
-
- $.ajax(
- type: 'PUT'
- url: issueUpdateURL
- data: data
- dataType: 'json'
- beforeSend: ->
- $loading.fadeIn()
- if isDropdown
- $dropdown.trigger('loading.gl.dropdown')
- $selectbox.hide()
- $value.css('display', '')
-
- cssClass = if Date.parse(mediumDate) then 'bold' else 'no-value'
- $valueContent.html("<span class='#{cssClass}'>#{mediumDate}</span>")
- $sidebarValue.html(mediumDate)
-
- if value isnt ''
- $('.js-remove-due-date-holder').removeClass 'hidden'
- else
- $('.js-remove-due-date-holder').addClass 'hidden'
- ).done (data) ->
- if isDropdown
- $dropdown.trigger('loaded.gl.dropdown')
- $dropdown.dropdown('toggle')
- $loading.fadeOut()
-
- $block.on 'click', '.js-remove-due-date', (e) ->
- e.preventDefault()
- $("input[name='#{fieldName}']").val ''
- addDueDate(false)
-
- $datePicker.datepicker(
- dateFormat: 'yy-mm-dd',
- defaultDate: $("input[name='#{fieldName}']").val()
- altField: "input[name='#{fieldName}']"
- onSelect: ->
- addDueDate(true)
- )
-
- $(document)
- .off 'click', '.ui-datepicker-header a'
- .on 'click', '.ui-datepicker-header a', (e) ->
- e.stopImmediatePropagation()
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
new file mode 100644
index 00000000000..ae3dde63da3
--- /dev/null
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -0,0 +1,14 @@
+(function() {
+ $.fn.extend({
+ disable: function() {
+ return $(this).attr('disabled', 'disabled').addClass('disabled');
+ }
+ });
+
+ $.fn.extend({
+ enable: function() {
+ return $(this).removeAttr('disabled').removeClass('disabled');
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/extensions/jquery.js.coffee b/app/assets/javascripts/extensions/jquery.js.coffee
deleted file mode 100644
index 0a9db8eb5ef..00000000000
--- a/app/assets/javascripts/extensions/jquery.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-# Disable an element and add the 'disabled' Bootstrap class
-$.fn.extend disable: ->
- $(@)
- .attr('disabled', 'disabled')
- .addClass('disabled')
-
-# Enable an element and remove the 'disabled' Bootstrap class
-$.fn.extend enable: ->
- $(@)
- .removeAttr('disabled')
- .removeClass('disabled')
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
new file mode 100644
index 00000000000..3fb3b1a8b51
--- /dev/null
+++ b/app/assets/javascripts/files_comment_button.js
@@ -0,0 +1,146 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.FilesCommentButton = (function() {
+ var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
+
+ COMMENT_BUTTON_CLASS = '.add-diff-note';
+
+ COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
+
+ LINE_HOLDER_CLASS = '.line_holder';
+
+ LINE_NUMBER_CLASS = 'diff-line-num';
+
+ LINE_CONTENT_CLASS = 'line_content';
+
+ UNFOLDABLE_LINE_CLASS = 'js-unfold';
+
+ EMPTY_CELL_CLASS = 'empty-cell';
+
+ OLD_LINE_CLASS = 'old_line';
+
+ LINE_COLUMN_CLASSES = "." + LINE_NUMBER_CLASS + ", .line_content";
+
+ TEXT_FILE_SELECTOR = '.text-file';
+
+ DEBOUNCE_TIMEOUT_DURATION = 100;
+
+ function FilesCommentButton(filesContainerElement) {
+ var debounce;
+ this.filesContainerElement = filesContainerElement;
+ this.destroy = bind(this.destroy, this);
+ this.render = bind(this.render, this);
+ this.VIEW_TYPE = $('input#view[type=hidden]').val();
+ debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
+ $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy);
+ }
+
+ FilesCommentButton.prototype.render = function(e) {
+ var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
+ $currentTarget = $(e.currentTarget);
+
+ buttonParentElement = this.getButtonParent($currentTarget);
+ if (!this.validateButtonParent(buttonParentElement)) return;
+ lineContentElement = this.getLineContent($currentTarget);
+ if (!this.validateLineContent(lineContentElement)) return;
+
+ textFileElement = this.getTextFileElement($currentTarget);
+ buttonParentElement.append(this.buildButton({
+ noteableType: textFileElement.attr('data-noteable-type'),
+ noteableID: textFileElement.attr('data-noteable-id'),
+ commitID: textFileElement.attr('data-commit-id'),
+ noteType: lineContentElement.attr('data-note-type'),
+ position: lineContentElement.attr('data-position'),
+ lineType: lineContentElement.attr('data-line-type'),
+ discussionID: lineContentElement.attr('data-discussion-id'),
+ lineCode: lineContentElement.attr('data-line-code')
+ }));
+ };
+
+ FilesCommentButton.prototype.destroy = function(e) {
+ if (this.isMovingToSameType(e)) {
+ return;
+ }
+ $(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove();
+ };
+
+ FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
+ var initializedButtonTemplate;
+ initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
+ COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
+ });
+ return $(initializedButtonTemplate).attr({
+ 'data-noteable-type': buttonAttributes.noteableType,
+ 'data-noteable-id': buttonAttributes.noteableID,
+ 'data-commit-id': buttonAttributes.commitID,
+ 'data-note-type': buttonAttributes.noteType,
+ 'data-line-code': buttonAttributes.lineCode,
+ 'data-position': buttonAttributes.position,
+ 'data-discussion-id': buttonAttributes.discussionID,
+ 'data-line-type': buttonAttributes.lineType
+ });
+ };
+
+ FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
+ return $(hoveredElement.closest(TEXT_FILE_SELECTOR));
+ };
+
+ FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
+ if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
+ return hoveredElement;
+ }
+ if (this.VIEW_TYPE === 'inline') {
+ return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
+ } else {
+ return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
+ }
+ };
+
+ FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
+ if (this.VIEW_TYPE === 'inline') {
+ if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
+ return hoveredElement;
+ }
+ return hoveredElement.parent().find("." + OLD_LINE_CLASS);
+ } else {
+ if (hoveredElement.hasClass(LINE_NUMBER_CLASS)) {
+ return hoveredElement;
+ }
+ return $(hoveredElement).prev("." + LINE_NUMBER_CLASS);
+ }
+ };
+
+ FilesCommentButton.prototype.isMovingToSameType = function(e) {
+ var newButtonParent;
+ newButtonParent = this.getButtonParent($(e.toElement));
+ if (!newButtonParent) {
+ return false;
+ }
+ return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
+ };
+
+ FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
+ return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0;
+ };
+
+ FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
+ return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+ };
+
+ return FilesCommentButton;
+
+ })();
+
+ $.fn.filesCommentButton = function() {
+ if (!(this && (this.parent().data('can-create-note') != null))) {
+ return;
+ }
+ return this.each(function() {
+ if (!$.data(this, 'filesCommentButton')) {
+ return $.data(this, 'filesCommentButton', new FilesCommentButton($(this)));
+ }
+ });
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
new file mode 100644
index 00000000000..c8a02d6fa15
--- /dev/null
+++ b/app/assets/javascripts/flash.js
@@ -0,0 +1,43 @@
+(function() {
+ this.Flash = (function() {
+ var hideFlash;
+
+ hideFlash = function() {
+ return $(this).fadeOut();
+ };
+
+ function Flash(message, type, parent) {
+ var flash, textDiv;
+ if (type == null) {
+ type = 'alert';
+ }
+ if (parent == null) {
+ parent = null;
+ }
+ if (parent) {
+ this.flashContainer = parent.find('.flash-container');
+ } else {
+ this.flashContainer = $('.flash-container-page');
+ }
+ this.flashContainer.html('');
+ flash = $('<div/>', {
+ "class": "flash-" + type
+ });
+ flash.on('click', hideFlash);
+ textDiv = $('<div/>', {
+ "class": 'flash-text',
+ text: message
+ });
+ textDiv.appendTo(flash);
+ if (this.flashContainer.parent().hasClass('content-wrapper')) {
+ textDiv.addClass('container-fluid container-limited');
+ }
+ flash.appendTo(this.flashContainer);
+ this.flashContainer.show();
+ }
+
+ return Flash;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee
deleted file mode 100644
index b76d214790a..00000000000
--- a/app/assets/javascripts/flash.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-class @Flash
- constructor: (message, type = 'alert')->
- @flash = $(".flash-container")
- @flash.html("")
-
- innerDiv = $('<div/>',
- class: "flash-#{type}"
- )
- innerDiv.appendTo(".flash-container")
-
- textDiv = $("<div/>",
- class: "flash-text",
- text: message
- )
- textDiv.appendTo(innerDiv)
-
- if @flash.parent().hasClass('content-wrapper')
- textDiv.addClass('container-fluid container-limited')
-
- @flash.click -> $(@).fadeOut()
- @flash.show()
-
- pinTo: (selector) ->
- @flash.detach().appendTo(selector)
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
deleted file mode 100644
index 4a851d9c9fb..00000000000
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ /dev/null
@@ -1,228 +0,0 @@
-# Creates the variables for setting up GFM auto-completion
-
-window.GitLab ?= {}
-GitLab.GfmAutoComplete =
- dataLoading: false
- dataLoaded: false
- cachedData: {}
- dataSource: ''
-
- # 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>'
-
- Labels:
- template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
-
- # Issues and MergeRequests
- Issues:
- template: '<li><small>${id}</small> ${title}</li>'
-
- # Milestones
- Milestones:
- template: '<li>${title}</li>'
-
- Loading:
- template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
-
- DefaultOptions:
- sorter: (query, items, searchKey) ->
- return items if items[0].name? and items[0].name is 'loading'
-
- $.fn.atwho.default.callbacks.sorter(query, items, searchKey)
- filter: (query, data, searchKey) ->
- return data if data[0] is 'loading'
-
- $.fn.atwho.default.callbacks.filter(query, data, searchKey)
- beforeInsert: (value) ->
- if not GitLab.GfmAutoComplete.dataLoaded
- @at
- else
- value
-
- # Add GFM auto-completion to all input fields, that accept GFM input.
- setup: (wrap) ->
- @input = $('.js-gfm-input')
-
- # destroy previous instances
- @destroyAtWho()
-
- # set up instances
- @setupAtWho()
-
- if @dataSource
- if not @dataLoading and not @cachedData
- @dataLoading = true
-
- # We should wait until initializations are done
- # and only trigger the last .setup since
- # The previous .dataSource belongs to the previous issuable
- # and the last one will have the **proper** .dataSource property
- # TODO: Make this a singleton and turn off events when moving to another page
- setTimeout( =>
- fetch = @fetchData(@dataSource)
- fetch.done (data) =>
- @dataLoading = false
- @loadData(data)
- , 1000)
-
- if @cachedData?
- @loadData(@cachedData)
-
- setupAtWho: ->
- # Emoji
- @input.atwho
- at: ':'
- displayTpl: (value) =>
- if value.path?
- @Emoji.template
- else
- @Loading.template
- insertTpl: ':${name}:'
- data: ['loading']
- callbacks:
- sorter: @DefaultOptions.sorter
- filter: @DefaultOptions.filter
- beforeInsert: @DefaultOptions.beforeInsert
-
- # Team Members
- @input.atwho
- at: '@'
- displayTpl: (value) =>
- if value.username?
- @Members.template
- else
- @Loading.template
- insertTpl: '${atwho-at}${username}'
- searchKey: 'search'
- data: ['loading']
- callbacks:
- sorter: @DefaultOptions.sorter
- filter: @DefaultOptions.filter
- beforeInsert: @DefaultOptions.beforeInsert
- beforeSave: (members) ->
- $.map members, (m) ->
- return m if not m.username?
-
- title = m.name
- title += " (#{m.count})" if m.count
-
- username: m.username
- title: sanitize(title)
- search: sanitize("#{m.username} #{m.name}")
-
- @input.atwho
- at: '#'
- alias: 'issues'
- searchKey: 'search'
- displayTpl: (value) =>
- if value.title?
- @Issues.template
- else
- @Loading.template
- data: ['loading']
- insertTpl: '${atwho-at}${id}'
- callbacks:
- sorter: @DefaultOptions.sorter
- filter: @DefaultOptions.filter
- beforeInsert: @DefaultOptions.beforeInsert
- beforeSave: (issues) ->
- $.map issues, (i) ->
- return i if not i.title?
-
- id: i.iid
- title: sanitize(i.title)
- search: "#{i.iid} #{i.title}"
-
- @input.atwho
- at: '%'
- alias: 'milestones'
- searchKey: 'search'
- displayTpl: (value) =>
- if value.title?
- @Milestones.template
- else
- @Loading.template
- insertTpl: '${atwho-at}"${title}"'
- data: ['loading']
- callbacks:
- beforeSave: (milestones) ->
- $.map milestones, (m) ->
- return m if not m.title?
-
- id: m.iid
- title: sanitize(m.title)
- search: "#{m.title}"
-
- @input.atwho
- at: '!'
- alias: 'mergerequests'
- searchKey: 'search'
- displayTpl: (value) =>
- if value.title?
- @Issues.template
- else
- @Loading.template
- data: ['loading']
- insertTpl: '${atwho-at}${id}'
- callbacks:
- sorter: @DefaultOptions.sorter
- filter: @DefaultOptions.filter
- beforeInsert: @DefaultOptions.beforeInsert
- beforeSave: (merges) ->
- $.map merges, (m) ->
- return m if not m.title?
-
- id: m.iid
- title: sanitize(m.title)
- search: "#{m.iid} #{m.title}"
-
- @input.atwho
- at: '~'
- alias: 'labels'
- searchKey: 'search'
- displayTpl: @Labels.template
- insertTpl: '${atwho-at}${title}'
- callbacks:
- beforeSave: (merges) ->
- sanitizeLabelTitle = (title)->
- if /[\w\?&]+\s+[\w\?&]+/g.test(title)
- "\"#{sanitize(title)}\""
- else
- sanitize(title)
-
- $.map merges, (m) ->
- title: sanitizeLabelTitle(m.title)
- color: m.color
- search: "#{m.title}"
-
- destroyAtWho: ->
- @input.atwho('destroy')
-
- fetchData: (dataSource) ->
- $.getJSON(dataSource)
-
- loadData: (data) ->
- @cachedData = data
- @dataLoaded = true
-
- # load members
- @input.atwho 'load', '@', data.members
- # load issues
- @input.atwho 'load', 'issues', data.issues
- # load milestones
- @input.atwho 'load', 'milestones', data.milestones
- # load merge requests
- @input.atwho 'load', 'mergerequests', data.mergerequests
- # load emojis
- @input.atwho 'load', ':', data.emojis
- # load labels
- @input.atwho 'load', '~', data.labels
-
- # This trigger at.js again
- # otherwise we would be stuck with loading until the user types
- $(':focus').trigger('keyup')
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
new file mode 100644
index 00000000000..3dca06d36b1
--- /dev/null
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -0,0 +1,335 @@
+(function() {
+ if (window.GitLab == null) {
+ window.GitLab = {};
+ }
+
+ GitLab.GfmAutoComplete = {
+ dataLoading: false,
+ dataLoaded: false,
+ cachedData: {},
+ dataSource: '',
+ Emoji: {
+ template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
+ },
+ Members: {
+ template: '<li>${username} <small>${title}</small></li>'
+ },
+ Labels: {
+ template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
+ },
+ Issues: {
+ template: '<li><small>${id}</small> ${title}</li>'
+ },
+ Milestones: {
+ template: '<li>${title}</li>'
+ },
+ Loading: {
+ template: '<li><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') {
+ return items;
+ }
+ return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
+ },
+ filter: function(query, data, searchKey) {
+ if (data[0] === 'loading') {
+ return data;
+ }
+ return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
+ },
+ beforeInsert: function(value) {
+ if (!GitLab.GfmAutoComplete.dataLoaded) {
+ return this.at;
+ } else {
+ return value;
+ }
+ }
+ },
+ setup: function(input) {
+ this.input = input || $('.js-gfm-input');
+ this.destroyAtWho();
+ this.setupAtWho();
+ if (this.dataSource) {
+ if (!this.dataLoading && !this.cachedData) {
+ this.dataLoading = true;
+ setTimeout((function(_this) {
+ return function() {
+ var fetch;
+ fetch = _this.fetchData(_this.dataSource);
+ return fetch.done(function(data) {
+ _this.dataLoading = false;
+ return _this.loadData(data);
+ });
+ };
+ })(this), 1000);
+ }
+ if (this.cachedData != null) {
+ return this.loadData(this.cachedData);
+ }
+ }
+ },
+ setupAtWho: function() {
+ this.input.atwho({
+ at: ':',
+ displayTpl: (function(_this) {
+ return function(value) {
+ if (value.path != null) {
+ return _this.Emoji.template;
+ } else {
+ return _this.Loading.template;
+ }
+ };
+ })(this),
+ insertTpl: ':${name}:',
+ data: ['loading'],
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert
+ }
+ });
+ this.input.atwho({
+ at: '@',
+ displayTpl: (function(_this) {
+ return function(value) {
+ if (value.username != null) {
+ return _this.Members.template;
+ } else {
+ return _this.Loading.template;
+ }
+ };
+ })(this),
+ insertTpl: '${atwho-at}${username}',
+ searchKey: 'search',
+ data: ['loading'],
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(members) {
+ return $.map(members, function(m) {
+ var title;
+ if (m.username == null) {
+ return m;
+ }
+ title = m.name;
+ if (m.count) {
+ title += " (" + m.count + ")";
+ }
+ return {
+ username: m.username,
+ title: sanitize(title),
+ search: sanitize(m.username + " " + m.name)
+ };
+ });
+ }
+ }
+ });
+ this.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'],
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(issues) {
+ return $.map(issues, function(i) {
+ if (i.title == null) {
+ return i;
+ }
+ return {
+ id: i.iid,
+ title: sanitize(i.title),
+ search: i.iid + " " + i.title
+ };
+ });
+ }
+ }
+ });
+ this.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'],
+ callbacks: {
+ beforeSave: function(milestones) {
+ return $.map(milestones, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: "" + m.title
+ };
+ });
+ }
+ }
+ });
+ this.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'],
+ insertTpl: '${atwho-at}${id}',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(merges) {
+ return $.map(merges, function(m) {
+ if (m.title == null) {
+ return m;
+ }
+ return {
+ id: m.iid,
+ title: sanitize(m.title),
+ search: m.iid + " " + m.title
+ };
+ });
+ }
+ }
+ });
+ this.input.atwho({
+ at: '~',
+ alias: 'labels',
+ searchKey: 'search',
+ displayTpl: this.Labels.template,
+ insertTpl: '${atwho-at}${title}',
+ callbacks: {
+ beforeSave: function(merges) {
+ var sanitizeLabelTitle;
+ sanitizeLabelTitle = function(title) {
+ if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
+ return "\"" + (sanitize(title)) + "\"";
+ } else {
+ return sanitize(title);
+ }
+ };
+ return $.map(merges, function(m) {
+ return {
+ title: sanitizeLabelTitle(m.title),
+ color: m.color,
+ search: "" + m.title
+ };
+ });
+ }
+ }
+ });
+ // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
+ this.input.filter('[data-supports-slash-commands="true"]').atwho({
+ at: '/',
+ alias: 'commands',
+ searchKey: 'search',
+ displayTpl: function(value) {
+ var tpl = '<li>/${name}';
+ if (value.aliases.length > 0) {
+ tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
+ }
+ if (value.params.length > 0) {
+ tpl += ' <small><%- params.join(" ") %></small>';
+ }
+ if (value.description !== '') {
+ tpl += '<small class="description"><i><%- description %></i></small>';
+ }
+ tpl += '</li>';
+ return _.template(tpl)(value);
+ },
+ insertTpl: function(value) {
+ var tpl = "/${name} ";
+ var reference_prefix = null;
+ if (value.params.length > 0) {
+ reference_prefix = value.params[0][0];
+ if (/^[@%~]/.test(reference_prefix)) {
+ tpl += '<%- reference_prefix %>';
+ }
+ }
+ return _.template(tpl)({ reference_prefix: reference_prefix });
+ },
+ suffix: '',
+ callbacks: {
+ sorter: this.DefaultOptions.sorter,
+ filter: this.DefaultOptions.filter,
+ beforeInsert: this.DefaultOptions.beforeInsert,
+ beforeSave: function(commands) {
+ return $.map(commands, function(c) {
+ var search = c.name;
+ if (c.aliases.length > 0) {
+ search = search + " " + c.aliases.join(" ");
+ }
+ return {
+ name: c.name,
+ aliases: c.aliases,
+ params: c.params,
+ description: c.description,
+ search: search
+ };
+ });
+ },
+ matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) {
+ var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi
+ var match = regexp.exec(subtext);
+ if (match) {
+ return match[1];
+ } else {
+ return null;
+ }
+ }
+ }
+ });
+ return;
+ },
+ destroyAtWho: function() {
+ return this.input.atwho('destroy');
+ },
+ fetchData: function(dataSource) {
+ return $.getJSON(dataSource);
+ },
+ loadData: function(data) {
+ this.cachedData = data;
+ this.dataLoaded = true;
+ this.input.atwho('load', '@', data.members);
+ this.input.atwho('load', 'issues', data.issues);
+ this.input.atwho('load', 'milestones', data.milestones);
+ this.input.atwho('load', 'mergerequests', data.mergerequests);
+ this.input.atwho('load', ':', data.emojis);
+ this.input.atwho('load', '~', data.labels);
+ this.input.atwho('load', '/', data.commands);
+ return $(':focus').trigger('keyup');
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
new file mode 100644
index 00000000000..5a2a8523d9f
--- /dev/null
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -0,0 +1,735 @@
+(function() {
+ var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
+ bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
+
+ GitLabDropdownFilter = (function() {
+ var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
+
+ BLUR_KEYCODES = [27, 40];
+
+ ARROW_KEY_CODES = [38, 40];
+
+ HAS_VALUE_CLASS = "has-value";
+
+ function GitLabDropdownFilter(input, options) {
+ var $clearButton, $inputContainer, ref, timeout;
+ this.input = input;
+ this.options = options;
+ 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) {
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input.val('').trigger('keyup').focus();
+ };
+ })(this));
+ timeout = "";
+ this.input
+ .on('keydown', function (e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault()
+ }
+ })
+ .on('keyup', function(e) {
+ var keyCode;
+ keyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(keyCode) >= 0) {
+ return;
+ }
+ if (this.input.val() !== "" && !$inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.addClass(HAS_VALUE_CLASS);
+ } else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
+ $inputContainer.removeClass(HAS_VALUE_CLASS);
+ }
+ if (keyCode === 13 && !options.elIsInput) {
+ return false;
+ }
+ if (this.options.remote) {
+ clearTimeout(timeout);
+ return timeout = setTimeout(function() {
+ var blurField = this.shouldBlur(keyCode);
+ if (blurField && this.filterInputBlur) {
+ this.input.blur();
+ }
+ return this.options.query(this.input.val(), function(data) {
+ return this.options.callback(data);
+ }.bind(this));
+ }.bind(this), 250);
+ } else {
+ return this.filter(this.input.val());
+ }
+ }.bind(this));
+ }
+
+ GitLabDropdownFilter.prototype.shouldBlur = function(keyCode) {
+ return BLUR_KEYCODES.indexOf(keyCode) >= 0;
+ };
+
+ GitLabDropdownFilter.prototype.filter = function(search_text) {
+ var data, elements, group, key, results, tmp;
+ if (this.options.onFilter) {
+ this.options.onFilter(search_text);
+ }
+ data = this.options.data();
+ if ((data != null) && !this.options.filterByText) {
+ results = data;
+ if (search_text !== '') {
+ if (_.isArray(data)) {
+ results = fuzzaldrinPlus.filter(data, search_text, {
+ key: this.options.keys
+ });
+ } else {
+ if (gl.utils.isObject(data)) {
+ results = {};
+ for (key in data) {
+ group = data[key];
+ tmp = fuzzaldrinPlus.filter(group, search_text, {
+ key: this.options.keys
+ });
+ if (tmp.length) {
+ results[key] = tmp.map(function(item) {
+ return item;
+ });
+ }
+ }
+ }
+ }
+ }
+ return this.options.callback(results);
+ } else {
+ elements = this.options.elements();
+ if (search_text) {
+ return elements.each(function() {
+ var $el, matches;
+ $el = $(this);
+ matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
+ if (!$el.is('.dropdown-header')) {
+ if (matches.length) {
+ return $el.show().removeClass('option-hidden');
+ } else {
+ return $el.hide().addClass('option-hidden');
+ }
+ }
+ });
+ } else {
+ return elements.show().removeClass('option-hidden');
+ }
+ }
+ };
+
+ return GitLabDropdownFilter;
+
+ })();
+
+ GitLabDropdownRemote = (function() {
+ function GitLabDropdownRemote(dataEndpoint, options) {
+ this.dataEndpoint = dataEndpoint;
+ this.options = options;
+ }
+
+ GitLabDropdownRemote.prototype.execute = function() {
+ if (typeof this.dataEndpoint === "string") {
+ return this.fetchData();
+ } else if (typeof this.dataEndpoint === "function") {
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+ return this.dataEndpoint("", (function(_this) {
+ return function(data) {
+ if (_this.options.success) {
+ _this.options.success(data);
+ }
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this));
+ }
+ };
+
+ GitLabDropdownRemote.prototype.fetchData = function() {
+ return $.ajax({
+ url: this.dataEndpoint,
+ dataType: this.options.dataType,
+ beforeSend: (function(_this) {
+ return function() {
+ if (_this.options.beforeSend) {
+ return _this.options.beforeSend();
+ }
+ };
+ })(this),
+ success: (function(_this) {
+ return function(data) {
+ if (_this.options.success) {
+ return _this.options.success(data);
+ }
+ };
+ })(this)
+ });
+ };
+
+ return GitLabDropdownRemote;
+
+ })();
+
+ GitLabDropdown = (function() {
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
+
+ LOADING_CLASS = "is-loading";
+
+ PAGE_TWO_CLASS = "is-page-two";
+
+ ACTIVE_CLASS = "is-active";
+
+ INDETERMINATE_CLASS = "is-indeterminate";
+
+ currentIndex = -1;
+
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5
+
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+
+ function GitLabDropdown(el1, options) {
+ var ref, ref1, ref2, ref3, searchFields, selector, self;
+ this.el = el1;
+ this.options = options;
+ this.updateLabel = bind(this.updateLabel, this);
+ this.hidden = bind(this.hidden, this);
+ this.opened = bind(this.opened, this);
+ this.shouldPropagate = bind(this.shouldPropagate, this);
+ self = this;
+ selector = $(this.el).data("target");
+ this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true;
+ self = this;
+ if (_.isString(this.filterInput)) {
+ this.filterInput = this.getElement(this.filterInput);
+ }
+ searchFields = this.options.search ? this.options.search.fields : [];
+ if (this.options.data) {
+ if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
+ this.fullData = this.options.data;
+ currentIndex = -1;
+ this.parseData(this.options.data);
+ } else {
+ this.remote = new GitLabDropdownRemote(this.options.data, {
+ dataType: this.options.dataType,
+ beforeSend: this.toggleLoading.bind(this),
+ success: (function(_this) {
+ return function(data) {
+ _this.fullData = data;
+ _this.parseData(_this.fullData);
+ if (_this.options.filterable && _this.filter && _this.filter.input) {
+ return _this.filter.input.trigger('keyup');
+ }
+ };
+ })(this)
+ });
+ }
+ }
+ if (this.options.filterable) {
+ this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
+ filterInputBlur: this.filterInputBlur,
+ filterByText: this.options.filterByText,
+ onFilter: this.options.onFilter,
+ remote: this.options.filterRemote,
+ query: this.options.data,
+ keys: searchFields,
+ elements: (function(_this) {
+ return function() {
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $(selector);
+ };
+ })(this),
+ data: (function(_this) {
+ return function() {
+ return _this.fullData;
+ };
+ })(this),
+ callback: (function(_this) {
+ return function(data) {
+ _this.parseData(data);
+ if (_this.filterInput.val() !== '') {
+ selector = SELECTABLE_CLASSES;
+ if (_this.dropdown.find('.dropdown-toggle-page').length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ if ($(_this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+ currentIndex = 0;
+ }
+ }
+ };
+ })(this)
+ });
+ }
+ this.dropdown.on("shown.bs.dropdown", this.opened);
+ this.dropdown.on("hidden.bs.dropdown", this.hidden);
+ $(this.el).on("update.label", this.updateLabel);
+ this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
+ this.dropdown.on('keyup', (function(_this) {
+ return function(e) {
+ if (e.which === 27) {
+ return $('.dropdown-menu-close', _this.dropdown).trigger('click');
+ }
+ };
+ })(this));
+ this.dropdown.on('blur', 'a', (function(_this) {
+ return function(e) {
+ var $dropdownMenu, $relatedTarget;
+ if (e.relatedTarget != null) {
+ $relatedTarget = $(e.relatedTarget);
+ $dropdownMenu = $relatedTarget.closest('.dropdown-menu');
+ if ($dropdownMenu.length === 0) {
+ return _this.dropdown.removeClass('open');
+ }
+ }
+ };
+ })(this));
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ this.dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on("click", (function(_this) {
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.togglePage();
+ };
+ })(this));
+ }
+ if (this.options.selectable) {
+ selector = ".dropdown-content a";
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one .dropdown-content a";
+ }
+ this.dropdown.on("click", selector, function(e) {
+ var $el, selected;
+ $el = $(this);
+ selected = self.rowClicked($el);
+ if (self.options.clicked) {
+ self.options.clicked(selected, $el, e);
+ }
+ return $el.trigger('blur');
+ });
+ }
+ }
+
+ GitLabDropdown.prototype.getElement = function(selector) {
+ return this.dropdown.find(selector);
+ };
+
+ GitLabDropdown.prototype.toggleLoading = function() {
+ return $('.dropdown-menu', this.dropdown).toggleClass(LOADING_CLASS);
+ };
+
+ GitLabDropdown.prototype.togglePage = function() {
+ var menu;
+ menu = $('.dropdown-menu', this.dropdown);
+ if (menu.hasClass(PAGE_TWO_CLASS)) {
+ if (this.remote) {
+ this.remote.execute();
+ }
+ }
+ menu.toggleClass(PAGE_TWO_CLASS);
+ return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
+ };
+
+ GitLabDropdown.prototype.parseData = function(data) {
+ var full_html, groupData, html, name;
+ this.renderedData = data;
+ if (this.options.filterable && data.length === 0) {
+ html = [this.noResults()];
+ } else {
+ if (gl.utils.isObject(data)) {
+ html = [];
+ for (name in data) {
+ groupData = data[name];
+ html.push(this.renderItem({
+ header: name
+ }, name));
+ this.renderData(groupData, name).map(function(item) {
+ return html.push(item);
+ });
+ }
+ } else {
+ html = this.renderData(data);
+ }
+ }
+ full_html = this.renderMenu(html);
+ return this.appendMenu(full_html);
+ };
+
+ GitLabDropdown.prototype.renderData = function(data, group) {
+ if (group == null) {
+ group = false;
+ }
+ return data.map((function(_this) {
+ return function(obj, index) {
+ return _this.renderItem(obj, group, index);
+ };
+ })(this));
+ };
+
+ GitLabDropdown.prototype.shouldPropagate = function(e) {
+ var $target;
+ if (this.options.multiSelect) {
+ $target = $(e.target);
+ if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ e.stopPropagation();
+ return false;
+ } else {
+ return true;
+ }
+ }
+ };
+
+ GitLabDropdown.prototype.opened = function() {
+ var contentHtml;
+ this.resetRows();
+ this.addArrowKeyEvent();
+ if (this.options.setIndeterminateIds) {
+ this.options.setIndeterminateIds.call(this);
+ }
+ if (this.options.setActiveIds) {
+ this.options.setActiveIds.call(this);
+ }
+ if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
+ this.parseData(this.fullData);
+ }
+ contentHtml = $('.dropdown-content', this.dropdown).html();
+ if (this.remote && contentHtml === "") {
+ this.remote.execute();
+ }
+ if (this.options.filterable) {
+ this.filterInput.focus();
+ }
+ return this.dropdown.trigger('shown.gl.dropdown');
+ };
+
+ GitLabDropdown.prototype.hidden = function(e) {
+ var $input;
+ this.resetRows();
+ this.removeArrayKeyEvent();
+ $input = this.dropdown.find(".dropdown-input-field");
+ if (this.options.filterable) {
+ $input.blur().val("");
+ }
+ if (!this.options.persistWhenHide) {
+ $input.trigger("keyup");
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
+ }
+ if (this.options.hidden) {
+ this.options.hidden.call(this, e);
+ }
+ return this.dropdown.trigger('hidden.gl.dropdown');
+ };
+
+ GitLabDropdown.prototype.renderMenu = function(html) {
+ var menu_html;
+ menu_html = "";
+ if (this.options.renderMenu) {
+ menu_html = this.options.renderMenu(html);
+ } else {
+ menu_html = $('<ul />').append(html);
+ }
+ return menu_html;
+ };
+
+ GitLabDropdown.prototype.appendMenu = function(html) {
+ var selector;
+ selector = '.dropdown-content';
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one .dropdown-content";
+ }
+ return $(selector, this.dropdown).empty().append(html);
+ };
+
+ GitLabDropdown.prototype.renderItem = function(data, group, index) {
+ var cssClass, field, fieldName, groupAttrs, html, selected, text, url, value;
+ if (group == null) {
+ group = false;
+ }
+ if (index == null) {
+ index = false;
+ }
+ html = "";
+ if (data === "divider") {
+ return "<li class='divider'></li>";
+ }
+ if (data === "separator") {
+ return "<li class='separator'></li>";
+ }
+ if (data.header != null) {
+ return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
+ }
+ if (this.options.renderRow) {
+ html = this.options.renderRow.call(this.options, data, this);
+ } else {
+ if (!selected) {
+ value = this.options.id ? this.options.id(data) : data.id;
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+ if (field.length) {
+ selected = true;
+ }
+ }
+ if (this.options.url != null) {
+ url = this.options.url(data);
+ } else {
+ url = data.url != null ? data.url : '#';
+ }
+ if (this.options.text != null) {
+ text = this.options.text(data);
+ } else {
+ text = data.text != null ? data.text : '';
+ }
+ cssClass = "";
+ if (selected) {
+ cssClass = "is-active";
+ }
+ if (this.highlight) {
+ text = this.highlightTextMatches(text, this.filterInput.val());
+ }
+ if (group) {
+ groupAttrs = 'data-group=' + group + ' data-index=' + index;
+ } else {
+ groupAttrs = '';
+ }
+ html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+ url: url,
+ groupAttrs: groupAttrs,
+ cssClass: cssClass,
+ text: text
+ });
+ }
+ return html;
+ };
+
+ GitLabDropdown.prototype.highlightTextMatches = function(text, term) {
+ var occurrences;
+ occurrences = fuzzaldrinPlus.match(text, term);
+ return text.split('').map(function(character, i) {
+ if (indexOf.call(occurrences, i) >= 0) {
+ return "<b>" + character + "</b>";
+ } else {
+ return character;
+ }
+ }).join('');
+ };
+
+ GitLabDropdown.prototype.noResults = function() {
+ var html;
+ return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
+ };
+
+ GitLabDropdown.prototype.rowClicked = function(el) {
+ var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+ isInput = $(this.el).is('input');
+ if (this.renderedData) {
+ groupName = el.data('group');
+ if (groupName) {
+ selectedIndex = el.data('index');
+ selectedObject = this.renderedData[groupName][selectedIndex];
+ } else {
+ selectedIndex = el.closest('li').index();
+ selectedObject = this.renderedData[selectedIndex];
+ }
+ }
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
+ value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+ if (isInput) {
+ field = $(this.el);
+ } else {
+ field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
+ }
+ if (el.hasClass(ACTIVE_CLASS)) {
+ el.removeClass(ACTIVE_CLASS);
+ if (isInput) {
+ field.val('');
+ } else {
+ field.remove();
+ }
+ } else if (el.hasClass(INDETERMINATE_CLASS)) {
+ el.addClass(ACTIVE_CLASS);
+ el.removeClass(INDETERMINATE_CLASS);
+ if (value == null) {
+ field.remove();
+ }
+ if (!field.length && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ }
+ } else {
+ if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
+ this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
+ if (!isInput) {
+ this.dropdown.parent().find("input[name='" + fieldName + "']").remove();
+ }
+ }
+ if (value == null) {
+ field.remove();
+ }
+ el.addClass(ACTIVE_CLASS);
+ if (value != null) {
+ if (!field.length && fieldName) {
+ this.addInput(fieldName, value, selectedObject);
+ } else {
+ field.val(value).trigger('change');
+ }
+ }
+ }
+
+ // Update label right after input has been added
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObject, el, this);
+ }
+
+ return selectedObject;
+ };
+
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+ var $input;
+ $input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
+ if (this.options.inputId != null) {
+ $input.attr('id', this.options.inputId);
+ }
+ if (selectedObject && selectedObject.type) {
+ $input.attr('data-type', selectedObject.type);
+ }
+ return this.dropdown.before($input);
+ };
+
+ GitLabDropdown.prototype.selectRowAtIndex = function(index) {
+ var $el, selector;
+ // If we pass an option index
+ if (typeof index !== "undefined") {
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+ } else {
+ selector = ".dropdown-content .is-focused";
+ }
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ $el = $(selector, this.dropdown);
+ if ($el.length) {
+ var href = $el.attr('href');
+ if (href && href !== '#') {
+ Turbolinks.visit(href);
+ } else {
+ $el.first().trigger('click');
+ }
+ }
+ };
+
+ GitLabDropdown.prototype.addArrowKeyEvent = function() {
+ var $input, ARROW_KEY_CODES, selector;
+ ARROW_KEY_CODES = [38, 40];
+ $input = this.dropdown.find(".dropdown-input-field");
+ selector = SELECTABLE_CLASSES;
+ if (this.dropdown.find(".dropdown-toggle-page").length) {
+ selector = ".dropdown-page-one " + selector;
+ }
+ return $('body').on('keydown', (function(_this) {
+ return function(e) {
+ var $listItems, PREV_INDEX, currentKeyCode;
+ currentKeyCode = e.which;
+ if (ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ PREV_INDEX = currentIndex;
+ $listItems = $(selector, _this.dropdown);
+ if (currentKeyCode === 40) {
+ if (currentIndex < ($listItems.length - 1)) {
+ currentIndex += 1;
+ }
+ } else if (currentKeyCode === 38) {
+ if (currentIndex > 0) {
+ currentIndex -= 1;
+ }
+ }
+ if (currentIndex !== PREV_INDEX) {
+ _this.highlightRowAtIndex($listItems, currentIndex);
+ }
+ return false;
+ }
+ if (currentKeyCode === 13 && currentIndex !== -1) {
+ _this.selectRowAtIndex();
+ }
+ };
+ })(this));
+ };
+
+ GitLabDropdown.prototype.removeArrayKeyEvent = function() {
+ return $('body').off('keydown');
+ };
+
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
+
+ GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
+ var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ $listItem = $listItems.eq(index);
+ $listItem.find('a:first-child').addClass("is-focused");
+ $dropdownContent = $listItem.closest('.dropdown-content');
+ dropdownScrollTop = $dropdownContent.scrollTop();
+ dropdownContentHeight = $dropdownContent.outerHeight();
+ dropdownContentTop = $dropdownContent.prop('offsetTop');
+ dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ listItemHeight = $listItem.outerHeight();
+ listItemTop = $listItem.prop('offsetTop');
+ listItemBottom = listItemTop + listItemHeight;
+ if (!index) {
+ $dropdownContent.scrollTop(0)
+ } else if (index === ($listItems.length - 1)) {
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
+ }
+ };
+
+ GitLabDropdown.prototype.updateLabel = function(selected, el, instance) {
+ if (selected == null) {
+ selected = null;
+ }
+ if (el == null) {
+ el = null;
+ }
+ if (instance == null) {
+ instance = null;
+ }
+ return $(this.el).find(".dropdown-toggle-text").text(this.options.toggleLabel(selected, el, instance));
+ };
+
+ return GitLabDropdown;
+
+ })();
+
+ $.fn.glDropdown = function(opts) {
+ return this.each(function() {
+ if (!$.data(this, 'glDropdown')) {
+ return $.data(this, 'glDropdown', new GitLabDropdown(this, opts));
+ }
+ });
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
deleted file mode 100644
index 1c65e833d47..00000000000
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ /dev/null
@@ -1,608 +0,0 @@
-class GitLabDropdownFilter
- BLUR_KEYCODES = [27, 40]
- ARROW_KEY_CODES = [38, 40]
- HAS_VALUE_CLASS = "has-value"
-
- constructor: (@input, @options) ->
- {
- @filterInputBlur = true
- } = @options
-
- $inputContainer = @input.parent()
- $clearButton = $inputContainer.find('.js-dropdown-input-clear')
-
- @indeterminateIds = []
-
- # Clear click
- $clearButton.on 'click', (e) =>
- e.preventDefault()
- e.stopPropagation()
- @input
- .val('')
- .trigger('keyup')
- .focus()
-
- # Key events
- timeout = ""
- @input.on "keyup", (e) =>
- keyCode = e.which
-
- return if ARROW_KEY_CODES.indexOf(keyCode) >= 0
-
- if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
- $inputContainer.addClass HAS_VALUE_CLASS
- else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
- $inputContainer.removeClass HAS_VALUE_CLASS
-
- if keyCode is 13
- return false
-
- # Only filter asynchronously only if option remote is set
- if @options.remote
- clearTimeout timeout
- timeout = setTimeout =>
- blur_field = @shouldBlur keyCode
-
- if blur_field and @filterInputBlur
- @input.blur()
-
- @options.query @input.val(), (data) =>
- @options.callback(data)
- , 250
- else
- @filter @input.val()
-
- shouldBlur: (keyCode) ->
- return BLUR_KEYCODES.indexOf(keyCode) >= 0
-
- filter: (search_text) ->
- @options.onFilter(search_text) if @options.onFilter
- data = @options.data()
-
- if data? and not @options.filterByText
- results = data
-
- if search_text isnt ''
- # When data is an array of objects therefore [object Array] e.g.
- # [
- # { prop: 'foo' },
- # { prop: 'baz' }
- # ]
- if _.isArray(data)
- results = fuzzaldrinPlus.filter(data, search_text,
- key: @options.keys
- )
- else
- # If data is grouped therefore an [object Object]. e.g.
- # {
- # groupName1: [
- # { prop: 'foo' },
- # { prop: 'baz' }
- # ],
- # groupName2: [
- # { prop: 'abc' },
- # { prop: 'def' }
- # ]
- # }
- if gl.utils.isObject data
- results = {}
- for key, group of data
- tmp = fuzzaldrinPlus.filter(group, search_text,
- key: @options.keys
- )
-
- if tmp.length
- results[key] = tmp.map (item) -> item
-
- @options.callback results
- else
- elements = @options.elements()
-
- if search_text
- elements.each ->
- $el = $(@)
- matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
-
- unless $el.is('.dropdown-header')
- if matches.length
- $el.show()
- else
- $el.hide()
- else
- elements.show()
-
-class GitLabDropdownRemote
- constructor: (@dataEndpoint, @options) ->
-
- execute: ->
- if typeof @dataEndpoint is "string"
- @fetchData()
- else if typeof @dataEndpoint is "function"
- if @options.beforeSend
- @options.beforeSend()
-
- # Fetch the data by calling the data funcfion
- @dataEndpoint "", (data) =>
- if @options.success
- @options.success(data)
-
- if @options.beforeSend
- @options.beforeSend()
-
- # Fetch the data through ajax if the data is a string
- fetchData: ->
- $.ajax(
- url: @dataEndpoint,
- dataType: @options.dataType,
- beforeSend: =>
- if @options.beforeSend
- @options.beforeSend()
- success: (data) =>
- if @options.success
- @options.success(data)
- )
-
-class GitLabDropdown
- LOADING_CLASS = "is-loading"
- PAGE_TWO_CLASS = "is-page-two"
- ACTIVE_CLASS = "is-active"
- INDETERMINATE_CLASS = "is-indeterminate"
- currentIndex = -1
-
- FILTER_INPUT = '.dropdown-input .dropdown-input-field'
-
- constructor: (@el, @options) ->
- self = @
- selector = $(@el).data "target"
- @dropdown = if selector? then $(selector) else $(@el).parent()
-
- # Set Defaults
- {
- # If no input is passed create a default one
- @filterInput = @getElement(FILTER_INPUT)
- @highlight = false
- @filterInputBlur = true
- } = @options
-
- self = @
-
- # If selector was passed
- if _.isString(@filterInput)
- @filterInput = @getElement(@filterInput)
-
- searchFields = if @options.search then @options.search.fields else [];
-
- if @options.data
- # If we provided data
- # data could be an array of objects or a group of arrays
- if _.isObject(@options.data) and not _.isFunction(@options.data)
- @fullData = @options.data
- @parseData @options.data
- else
- # Remote data
- @remote = new GitLabDropdownRemote @options.data, {
- dataType: @options.dataType,
- beforeSend: @toggleLoading.bind(@)
- success: (data) =>
- @fullData = data
-
- @parseData @fullData
-
- @filter.input.trigger('keyup') if @options.filterable and @filter and @filter.input
- }
-
- # Init filterable
- if @options.filterable
- @filter = new GitLabDropdownFilter @filterInput,
- filterInputBlur: @filterInputBlur
- filterByText: @options.filterByText
- onFilter: @options.onFilter
- remote: @options.filterRemote
- query: @options.data
- keys: searchFields
- elements: =>
- selector = '.dropdown-content li:not(.divider)'
-
- if @dropdown.find('.dropdown-toggle-page').length
- selector = ".dropdown-page-one #{selector}"
-
- return $(selector)
- data: =>
- return @fullData
- callback: (data) =>
- currentIndex = -1
- @parseData data
-
- # Event listeners
-
- @dropdown.on "shown.bs.dropdown", @opened
- @dropdown.on "hidden.bs.dropdown", @hidden
- $(@el).on "update.label", @updateLabel
- @dropdown.on "click", ".dropdown-menu, .dropdown-menu-close", @shouldPropagate
- @dropdown.on 'keyup', (e) =>
- if e.which is 27 # Escape key
- $('.dropdown-menu-close', @dropdown).trigger 'click'
- @dropdown.on 'blur', 'a', (e) =>
- if e.relatedTarget?
- $relatedTarget = $(e.relatedTarget)
- $dropdownMenu = $relatedTarget.closest('.dropdown-menu')
-
- if $dropdownMenu.length is 0
- @dropdown.removeClass('open')
-
- if @dropdown.find(".dropdown-toggle-page").length
- @dropdown.find(".dropdown-toggle-page, .dropdown-menu-back").on "click", (e) =>
- e.preventDefault()
- e.stopPropagation()
-
- @togglePage()
-
- if @options.selectable
- selector = ".dropdown-content a"
-
- if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one .dropdown-content a"
-
- @dropdown.on "click", selector, (e) ->
- $el = $(@)
- selected = self.rowClicked $el
-
- if self.options.clicked
- self.options.clicked(selected, $el, e)
-
- # Finds an element inside wrapper element
- getElement: (selector) ->
- @dropdown.find selector
-
- toggleLoading: ->
- $('.dropdown-menu', @dropdown).toggleClass LOADING_CLASS
-
- togglePage: ->
- menu = $('.dropdown-menu', @dropdown)
-
- if menu.hasClass(PAGE_TWO_CLASS)
- if @remote
- @remote.execute()
-
- menu.toggleClass PAGE_TWO_CLASS
-
- # Focus first visible input on active page
- @dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus()
-
- parseData: (data) ->
- @renderedData = data
-
- if @options.filterable and data.length is 0
- # render no matching results
- html = [@noResults()]
- else
- # Handle array groups
- if gl.utils.isObject data
- html = []
- for name, groupData of data
- # Add header for each group
- html.push(@renderItem(header: name, name))
-
- @renderData(groupData, name)
- .map (item) ->
- html.push item
- else
- # Render each row
- html = @renderData(data)
-
- # Render the full menu
- full_html = @renderMenu(html)
-
- @appendMenu(full_html)
-
- renderData: (data, group = false) ->
- data.map (obj, index) =>
- return @renderItem(obj, group, index)
-
- shouldPropagate: (e) =>
- if @options.multiSelect
- $target = $(e.target)
-
- if not $target.hasClass('dropdown-menu-close') and not $target.hasClass('dropdown-menu-close-icon') and not $target.data('is-link')
- e.stopPropagation()
- return false
- else
- return true
-
- opened: =>
- @addArrowKeyEvent()
-
- if @options.setIndeterminateIds
- @options.setIndeterminateIds.call(@)
-
- if @options.setActiveIds
- @options.setActiveIds.call(@)
-
- # Makes indeterminate items effective
- if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
- @parseData @fullData
-
- contentHtml = $('.dropdown-content', @dropdown).html()
- if @remote && contentHtml is ""
- @remote.execute()
-
- if @options.filterable
- @filterInput.focus()
-
- @dropdown.trigger('shown.gl.dropdown')
-
- hidden: (e) =>
- @removeArrayKeyEvent()
-
- $input = @dropdown.find(".dropdown-input-field")
-
- if @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 not @options.persistWhenHide
- $input.trigger("keyup")
-
- if @dropdown.find(".dropdown-toggle-page").length
- $('.dropdown-menu', @dropdown).removeClass PAGE_TWO_CLASS
-
- if @options.hidden
- @options.hidden.call(@,e)
-
- @dropdown.trigger('hidden.gl.dropdown')
-
-
- # Render the full menu
- renderMenu: (html) ->
- menu_html = ""
-
- if @options.renderMenu
- menu_html = @options.renderMenu(html)
- else
- menu_html = $('<ul />')
- .append(html)
-
- return menu_html
-
- # Append the menu into the dropdown
- appendMenu: (html) ->
- selector = '.dropdown-content'
- if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one .dropdown-content"
- $(selector, @dropdown)
- .empty()
- .append(html)
-
- # Render the row
- renderItem: (data, group = false, index = false) ->
- html = ""
-
- # Divider
- return "<li class='divider'></li>" if data is "divider"
-
- # Separator is a full-width divider
- return "<li class='separator'></li>" if data is "separator"
-
- # Header
- return "<li class='dropdown-header'>#{data.header}</li>" if data.header?
-
- if @options.renderRow
- # Call the render function
- html = @options.renderRow.call(@options, data, @)
- else
- if not selected
- value = if @options.id then @options.id(data) else data.id
- fieldName = @options.fieldName
- field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
- if field.length
- selected = true
-
- # Set URL
- if @options.url?
- url = @options.url(data)
- else
- url = if data.url? then data.url else '#'
-
- # Set Text
- if @options.text?
- text = @options.text(data)
- else
- text = if data.text? then data.text else ''
-
- cssClass = "";
-
- if selected
- cssClass = "is-active"
-
- if @highlight
- text = @highlightTextMatches(text, @filterInput.val())
-
- if group
- groupAttrs = "data-group='#{group}' data-index='#{index}'"
- else
- groupAttrs = ''
-
- html = "<li>
- <a href='#{url}' #{groupAttrs} class='#{cssClass}'>
- #{text}
- </a>
- </li>"
-
- return html
-
- highlightTextMatches: (text, term) ->
- occurrences = fuzzaldrinPlus.match(text, term)
- text.split('').map((character, i) ->
- if i in occurrences then "<b>#{character}</b>" else character
- ).join('')
-
- noResults: ->
- html = "<li class='dropdown-menu-empty-link'>
- <a href='#' class='is-focused'>
- No matching results.
- </a>
- </li>"
-
- highlightRow: (index) ->
- if @filterInput.val() isnt ""
- selector = '.dropdown-content li:first-child a'
- if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one .dropdown-content li:first-child a"
-
- @getElement(selector).addClass 'is-focused'
-
- rowClicked: (el) ->
- fieldName = @options.fieldName
- if @renderedData
- groupName = el.data('group')
- if groupName
- selectedIndex = el.data('index')
- selectedObject = @renderedData[groupName][selectedIndex]
- else
- selectedIndex = el.closest('li').index()
- selectedObject = @renderedData[selectedIndex]
-
- value = if @options.id then @options.id(selectedObject, el) else selectedObject.id
- field = @dropdown.parent().find("input[name='#{fieldName}'][value='#{value}']")
- if el.hasClass(ACTIVE_CLASS)
- el.removeClass(ACTIVE_CLASS)
- field.remove()
-
- # Toggle the dropdown label
- if @options.toggleLabel
- @updateLabel(selectedObject, el, @)
- else
- selectedObject
- else if el.hasClass(INDETERMINATE_CLASS)
- el.addClass ACTIVE_CLASS
- el.removeClass INDETERMINATE_CLASS
-
- if not value?
- field.remove()
-
- if not field.length and fieldName
- @addInput(fieldName, value)
-
- return selectedObject
- else
- if not @options.multiSelect or el.hasClass('dropdown-clear-active')
- @dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
- @dropdown.parent().find("input[name='#{fieldName}']").remove()
-
- if !value?
- field.remove()
-
- # Toggle active class for the tick mark
- el.addClass ACTIVE_CLASS
-
- # Toggle the dropdown label
- if @options.toggleLabel
- @updateLabel(selectedObject, el, @)
- if value?
- if !field.length and fieldName
- @addInput(fieldName, value)
- else
- field.val value
-
- return selectedObject
-
- addInput: (fieldName, value)->
- # Create hidden input for form
- $input = $('<input>').attr('type', 'hidden')
- .attr('name', fieldName)
- .val(value)
-
- if @options.inputId?
- $input.attr('id', @options.inputId)
-
- @dropdown.before $input
-
- selectRowAtIndex: (e, index) ->
- selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(#{index}) a"
-
- if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one #{selector}"
-
- # simulate a click on the first link
- $el = $(selector, @dropdown)
-
- if $el.length
- e.preventDefault()
- e.stopImmediatePropagation()
- $el.first().trigger('click')
-
- addArrowKeyEvent: ->
- ARROW_KEY_CODES = [38, 40]
- $input = @dropdown.find(".dropdown-input-field")
-
- selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)'
- if @dropdown.find(".dropdown-toggle-page").length
- selector = ".dropdown-page-one #{selector}"
-
- $('body').on 'keydown', (e) =>
- currentKeyCode = e.which
-
- if ARROW_KEY_CODES.indexOf(currentKeyCode) >= 0
- e.preventDefault()
- e.stopImmediatePropagation()
-
- PREV_INDEX = currentIndex
- $listItems = $(selector, @dropdown)
-
- # if @options.filterable
- # $input.blur()
-
- if currentKeyCode is 40
- # Move down
- currentIndex += 1 if currentIndex < ($listItems.length - 1)
- else if currentKeyCode is 38
- # Move up
- currentIndex -= 1 if currentIndex > 0
-
- @highlightRowAtIndex($listItems, currentIndex) if currentIndex isnt PREV_INDEX
-
- return false
-
- if currentKeyCode is 13 and currentIndex isnt -1
- @selectRowAtIndex e, currentIndex
-
- removeArrayKeyEvent: ->
- $('body').off 'keydown'
-
- highlightRowAtIndex: ($listItems, index) ->
- # Remove the class for the previously focused row
- $('.is-focused', @dropdown).removeClass 'is-focused'
-
- # Update the class for the row at the specific index
- $listItem = $listItems.eq(index)
- $listItem.find('a:first-child').addClass "is-focused"
-
- # Dropdown content scroll area
- $dropdownContent = $listItem.closest('.dropdown-content')
- dropdownScrollTop = $dropdownContent.scrollTop()
- dropdownContentHeight = $dropdownContent.outerHeight()
- dropdownContentTop = $dropdownContent.prop('offsetTop')
- dropdownContentBottom = dropdownContentTop + dropdownContentHeight
-
- # Get the offset bottom of the list item
- listItemHeight = $listItem.outerHeight()
- listItemTop = $listItem.prop('offsetTop')
- listItemBottom = listItemTop + listItemHeight
-
- if listItemBottom > dropdownContentBottom + dropdownScrollTop
- # Scroll the dropdown content down
- $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom)
- else if listItemTop < dropdownContentTop + dropdownScrollTop
- # Scroll the dropdown content up
- $dropdownContent.scrollTop(listItemTop - dropdownContentTop)
-
- updateLabel: (selected = null, el = null, instance = null) =>
- $(@el).find(".dropdown-toggle-text").text @options.toggleLabel(selected, el, instance)
-
-$.fn.glDropdown = (opts) ->
- return @.each ->
- if (!$.data @, 'glDropdown')
- $.data(@, 'glDropdown', new GitLabDropdown @, opts)
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
new file mode 100644
index 00000000000..528a673eb15
--- /dev/null
+++ b/app/assets/javascripts/gl_form.js
@@ -0,0 +1,53 @@
+(function() {
+ this.GLForm = (function() {
+ function GLForm(form) {
+ this.form = form;
+ this.textarea = this.form.find('textarea.js-gfm-input');
+ this.destroy();
+ this.setupForm();
+ this.form.data('gl-form', this);
+ }
+
+ GLForm.prototype.destroy = function() {
+ this.clearEventListeners();
+ return this.form.data('gl-form', null);
+ };
+
+ GLForm.prototype.setupForm = function() {
+ var isNewForm;
+ isNewForm = this.form.is(':not(.gfm-form)');
+ this.form.removeClass('js-new-note-form');
+ if (isNewForm) {
+ this.form.find('.div-dropzone').remove();
+ this.form.addClass('gfm-form');
+ disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+ new DropzoneInput(this.form);
+ autosize(this.textarea);
+ this.addEventListeners();
+ gl.text.init(this.form);
+ }
+ this.form.find('.js-note-discard').hide();
+ return this.form.show();
+ };
+
+ GLForm.prototype.clearEventListeners = function() {
+ this.textarea.off('focus');
+ this.textarea.off('blur');
+ return gl.text.removeListeners(this.form);
+ };
+
+ GLForm.prototype.addEventListeners = function() {
+ this.textarea.on('focus', function() {
+ return $(this).closest('.md-area').addClass('is-focused');
+ });
+ return this.textarea.on('blur', function() {
+ return $(this).closest('.md-area').removeClass('is-focused');
+ });
+ };
+
+ return GLForm;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee
deleted file mode 100644
index 77512d187c9..00000000000
--- a/app/assets/javascripts/gl_form.js.coffee
+++ /dev/null
@@ -1,54 +0,0 @@
-class @GLForm
- constructor: (@form) ->
- @textarea = @form.find('textarea.js-gfm-input')
-
- # Before we start, we should clean up any previous data for this form
- @destroy()
-
- # Setup the form
- @setupForm()
-
- @form.data 'gl-form', @
-
- destroy: ->
- # Clean form listeners
- @clearEventListeners()
- @form.data 'gl-form', null
-
- setupForm: ->
- isNewForm = @form.is(':not(.gfm-form)')
-
- @form.removeClass 'js-new-note-form'
-
- if isNewForm
- @form.find('.div-dropzone').remove()
- @form.addClass('gfm-form')
- disableButtonIfEmptyField @form.find('.js-note-text'), @form.find('.js-comment-button')
-
- # remove notify commit author checkbox for non-commit notes
- GitLab.GfmAutoComplete.setup()
- new DropzoneInput(@form)
-
- autosize(@textarea)
-
- # form and textarea event listeners
- @addEventListeners()
-
- gl.text.init(@form)
-
- # hide discard button
- @form.find('.js-note-discard').hide()
-
- @form.show()
-
- clearEventListeners: ->
- @textarea.off 'focus'
- @textarea.off 'blur'
- gl.text.removeListeners(@form)
-
- addEventListeners: ->
- @textarea.on 'focus', ->
- $(@).closest('.md-area').addClass 'is-focused'
-
- @textarea.on 'blur', ->
- $(@).closest('.md-area').removeClass 'is-focused'
diff --git a/app/assets/javascripts/graphs/application.js.coffee b/app/assets/javascripts/graphs/application.js.coffee
deleted file mode 100644
index e0f681acf0b..00000000000
--- a/app/assets/javascripts/graphs/application.js.coffee
+++ /dev/null
@@ -1,7 +0,0 @@
-# This is a manifest file that'll be compiled into including all the files listed below.
-# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-# be included in the compiled file accessible from http://example.com/assets/application.js
-# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-# the compiled file.
-#
-#= require_tree .
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
new file mode 100644
index 00000000000..b95faadc8e7
--- /dev/null
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -0,0 +1,7 @@
+
+/*= require_tree . */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
new file mode 100644
index 00000000000..f041980bc19
--- /dev/null
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -0,0 +1,19 @@
+(function() {
+ this.StatGraph = (function() {
+ function StatGraph() {}
+
+ StatGraph.log = {};
+
+ StatGraph.get_log = function() {
+ return this.log;
+ };
+
+ StatGraph.set_log = function(data) {
+ return this.log = data;
+ };
+
+ return StatGraph;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph.js.coffee
deleted file mode 100644
index f36c71fd25e..00000000000
--- a/app/assets/javascripts/graphs/stat_graph.js.coffee
+++ /dev/null
@@ -1,6 +0,0 @@
-class @StatGraph
- @log: {}
- @get_log: ->
- @log
- @set_log: (data) ->
- @log = data
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
new file mode 100644
index 00000000000..927d241b357
--- /dev/null
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -0,0 +1,112 @@
+
+/*= require d3 */
+
+(function() {
+ this.ContributorsStatGraph = (function() {
+ function ContributorsStatGraph() {}
+
+ ContributorsStatGraph.prototype.init = function(log) {
+ var author_commits, total_commits;
+ this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
+ this.set_current_field("commits");
+ total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
+ this.add_master_graph(total_commits);
+ this.add_authors_graph(author_commits);
+ return this.change_date_header();
+ };
+
+ ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
+ this.master_graph = new ContributorsMasterGraph(total_data);
+ return this.master_graph.draw();
+ };
+
+ ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
+ var limited_author_data;
+ this.authors = [];
+ limited_author_data = author_data.slice(0, 100);
+ return _.each(limited_author_data, (function(_this) {
+ return function(d) {
+ var author_graph, author_header;
+ author_header = _this.create_author_header(d);
+ $(".contributors-list").append(author_header);
+ _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates);
+ return author_graph.draw();
+ };
+ })(this));
+ };
+
+ ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
+ var commits;
+ commits = $('<span/>', {
+ "class": 'graph-author-commits-count'
+ });
+ commits.text(author.commits + " commits");
+ return $('<span/>').append(commits);
+ };
+
+ ContributorsStatGraph.prototype.create_author_header = function(author) {
+ var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
+ list_item = $('<li/>', {
+ "class": 'person',
+ style: 'display: block;'
+ });
+ author_name = $('<h4>' + author.author_name + '</h4>');
+ author_email = $('<p class="graph-author-email">' + author.author_email + '</p>');
+ author_commit_info_span = $('<span/>', {
+ "class": 'commits'
+ });
+ author_commit_info = this.format_author_commit_info(author);
+ author_commit_info_span.html(author_commit_info);
+ list_item.append(author_name);
+ list_item.append(author_email);
+ list_item.append(author_commit_info_span);
+ return list_item;
+ };
+
+ ContributorsStatGraph.prototype.redraw_master = function() {
+ var total_data;
+ total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
+ this.master_graph.set_data(total_data);
+ return this.master_graph.redraw();
+ };
+
+ ContributorsStatGraph.prototype.redraw_authors = function() {
+ var author_commits, x_domain;
+ $("ol").html("");
+ x_domain = ContributorsGraph.prototype.x_domain;
+ author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field, x_domain);
+ return _.each(author_commits, (function(_this) {
+ return function(d) {
+ _this.redraw_author_commit_info(d);
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ };
+ })(this));
+ };
+
+ ContributorsStatGraph.prototype.set_current_field = function(field) {
+ return this.field = field;
+ };
+
+ ContributorsStatGraph.prototype.change_date_header = function() {
+ var print, print_date_format, x_domain;
+ x_domain = ContributorsGraph.prototype.x_domain;
+ print_date_format = d3.time.format("%B %e %Y");
+ print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]);
+ return $("#date_header").text(print);
+ };
+
+ ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
+ var author_commit_info, author_list_item;
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ };
+
+ return ContributorsStatGraph;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
deleted file mode 100644
index 1d9fae7cf79..00000000000
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-#= require d3
-
-class @ContributorsStatGraph
- init: (log) ->
- @parsed_log = ContributorsStatGraphUtil.parse_log(log)
- @set_current_field("commits")
- total_commits = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
- author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field)
- @add_master_graph(total_commits)
- @add_authors_graph(author_commits)
- @change_date_header()
- add_master_graph: (total_data) ->
- @master_graph = new ContributorsMasterGraph(total_data)
- @master_graph.draw()
- add_authors_graph: (author_data) ->
- @authors = []
- limited_author_data = author_data.slice(0, 100)
- _.each(limited_author_data, (d) =>
- author_header = @create_author_header(d)
- $(".contributors-list").append(author_header)
- @authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates)
- author_graph.draw()
- )
- format_author_commit_info: (author) ->
- commits = $('<span/>', {
- class: 'graph-author-commits-count'
- })
- commits.text(author.commits + " commits")
- $('<span/>').append(commits)
-
- create_author_header: (author) ->
- list_item = $('<li/>', {
- class: 'person'
- style: 'display: block;'
- })
- author_name = $('<h4>' + author.author_name + '</h4>')
- author_email = $('<p class="graph-author-email">' + author.author_email + '</p>')
- author_commit_info_span = $('<span/>', {
- class: 'commits'
- })
- author_commit_info = @format_author_commit_info(author)
- author_commit_info_span.html(author_commit_info)
- list_item.append(author_name)
- list_item.append(author_email)
- list_item.append(author_commit_info_span)
- list_item
- redraw_master: ->
- total_data = ContributorsStatGraphUtil.get_total_data(@parsed_log, @field)
- @master_graph.set_data(total_data)
- @master_graph.redraw()
- redraw_authors: ->
- $("ol").html("")
- x_domain = ContributorsGraph.prototype.x_domain
- author_commits = ContributorsStatGraphUtil.get_author_data(@parsed_log, @field, x_domain)
- _.each(author_commits, (d) =>
- @redraw_author_commit_info(d)
- $(@authors[d.author_name].list_item).appendTo("ol")
- @authors[d.author_name].set_data(d.dates)
- @authors[d.author_name].redraw()
- )
- set_current_field: (field) ->
- @field = field
- change_date_header: ->
- x_domain = ContributorsGraph.prototype.x_domain
- print_date_format = d3.time.format("%B %e %Y")
- print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1])
- $("#date_header").text(print)
- redraw_author_commit_info: (author) ->
- author_list_item = $(@authors[author.author_name].list_item)
- author_commit_info = @format_author_commit_info(author)
- author_list_item.find("span").html(author_commit_info)
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
new file mode 100644
index 00000000000..a646ca1d84f
--- /dev/null
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -0,0 +1,279 @@
+
+/*= require d3 */
+
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ContributorsGraph = (function() {
+ function ContributorsGraph() {}
+
+ ContributorsGraph.prototype.MARGIN = {
+ top: 20,
+ right: 20,
+ bottom: 30,
+ left: 50
+ };
+
+ ContributorsGraph.prototype.x_domain = null;
+
+ ContributorsGraph.prototype.y_domain = null;
+
+ ContributorsGraph.prototype.dates = [];
+
+ ContributorsGraph.set_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = data;
+ };
+
+ ContributorsGraph.set_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ var ref, ref1;
+ return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_x_domain = function(data) {
+ return ContributorsGraph.prototype.x_domain = d3.extent(data, function(d) {
+ return d.date;
+ });
+ };
+
+ ContributorsGraph.init_y_domain = function(data) {
+ return ContributorsGraph.prototype.y_domain = [
+ 0, d3.max(data, function(d) {
+ var ref, ref1;
+ return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ })
+ ];
+ };
+
+ ContributorsGraph.init_domain = function(data) {
+ ContributorsGraph.init_x_domain(data);
+ return ContributorsGraph.init_y_domain(data);
+ };
+
+ ContributorsGraph.set_dates = function(data) {
+ return ContributorsGraph.prototype.dates = data;
+ };
+
+ ContributorsGraph.prototype.set_x_domain = function() {
+ return this.x.domain(this.x_domain);
+ };
+
+ ContributorsGraph.prototype.set_y_domain = function() {
+ return this.y.domain(this.y_domain);
+ };
+
+ ContributorsGraph.prototype.set_domain = function() {
+ this.set_x_domain();
+ return this.set_y_domain();
+ };
+
+ ContributorsGraph.prototype.create_scale = function(width, height) {
+ this.x = d3.time.scale().range([0, width]).clamp(true);
+ return this.y = d3.scale.linear().range([height, 0]).nice();
+ };
+
+ ContributorsGraph.prototype.draw_x_axis = function() {
+ return this.svg.append("g").attr("class", "x axis").attr("transform", "translate(0, " + this.height + ")").call(this.x_axis);
+ };
+
+ ContributorsGraph.prototype.draw_y_axis = function() {
+ return this.svg.append("g").attr("class", "y axis").call(this.y_axis);
+ };
+
+ ContributorsGraph.prototype.set_data = function(data) {
+ return this.data = data;
+ };
+
+ return ContributorsGraph;
+
+ })();
+
+ this.ContributorsMasterGraph = (function(superClass) {
+ extend(ContributorsMasterGraph, superClass);
+
+ function ContributorsMasterGraph(data1) {
+ this.data = data1;
+ this.update_content = bind(this.update_content, this);
+ this.width = $('.content').width() - 70;
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.brush = null;
+ this.x_max_domain = null;
+ }
+
+ ContributorsMasterGraph.prototype.process_dates = function(data) {
+ var dates;
+ dates = this.get_dates(data);
+ this.parse_dates(data);
+ return ContributorsGraph.set_dates(dates);
+ };
+
+ ContributorsMasterGraph.prototype.get_dates = function(data) {
+ return _.pluck(data, 'date');
+ };
+
+ ContributorsMasterGraph.prototype.parse_dates = function(data) {
+ var parseDate;
+ parseDate = d3.time.format("%Y-%m-%d").parse;
+ return data.forEach(function(d) {
+ return d.date = parseDate(d.date);
+ });
+ };
+
+ ContributorsMasterGraph.prototype.create_scale = function() {
+ return ContributorsMasterGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsMasterGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom");
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsMasterGraph.prototype.create_svg = function() {
+ return this.svg = d3.select("#contributors-master").append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "tint-box").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsMasterGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
+ return x(d.date);
+ }).y0(this.height).y1(function(d) {
+ var ref, ref1, xa;
+ xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ return y(xa);
+ }).interpolate("basis");
+ };
+
+ ContributorsMasterGraph.prototype.create_brush = function() {
+ return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ };
+
+ ContributorsMasterGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area").attr("d", this.area);
+ };
+
+ ContributorsMasterGraph.prototype.add_brush = function() {
+ return this.svg.append("g").attr("class", "selection").call(this.brush).selectAll("rect").attr("height", this.height);
+ };
+
+ ContributorsMasterGraph.prototype.update_content = function() {
+ ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ return $("#brush_change").trigger('change');
+ };
+
+ ContributorsMasterGraph.prototype.draw = function() {
+ this.process_dates(this.data);
+ this.create_scale();
+ this.create_axes();
+ ContributorsGraph.init_domain(this.data);
+ this.x_max_domain = this.x_domain;
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.create_brush();
+ this.draw_path(this.data);
+ this.draw_x_axis();
+ this.draw_y_axis();
+ return this.add_brush();
+ };
+
+ ContributorsMasterGraph.prototype.redraw = function() {
+ this.process_dates(this.data);
+ ContributorsGraph.set_y_domain(this.data);
+ this.set_y_domain();
+ this.svg.select("path").datum(this.data);
+ this.svg.select("path").attr("d", this.area);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsMasterGraph;
+
+ })(ContributorsGraph);
+
+ this.ContributorsAuthorGraph = (function(superClass) {
+ extend(ContributorsAuthorGraph, superClass);
+
+ function ContributorsAuthorGraph(data1) {
+ this.data = data1;
+ if ($(window).width() < 768) {
+ this.width = $('.content').width() - 80;
+ } else {
+ this.width = ($('.content').width() / 2) - 100;
+ }
+ this.height = 200;
+ this.x = null;
+ this.y = null;
+ this.x_axis = null;
+ this.y_axis = null;
+ this.area = null;
+ this.svg = null;
+ this.list_item = null;
+ }
+
+ ContributorsAuthorGraph.prototype.create_scale = function() {
+ return ContributorsAuthorGraph.__super__.create_scale.call(this, this.width, this.height);
+ };
+
+ ContributorsAuthorGraph.prototype.create_axes = function() {
+ this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8);
+ return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ };
+
+ ContributorsAuthorGraph.prototype.create_area = function(x, y) {
+ return this.area = d3.svg.area().x(function(d) {
+ var parseDate;
+ parseDate = d3.time.format("%Y-%m-%d").parse;
+ return x(parseDate(d));
+ }).y0(this.height).y1((function(_this) {
+ return function(d) {
+ if (_this.data[d] != null) {
+ return y(_this.data[d]);
+ } else {
+ return y(0);
+ }
+ };
+ })(this)).interpolate("basis");
+ };
+
+ ContributorsAuthorGraph.prototype.create_svg = function() {
+ this.list_item = d3.selectAll(".person")[0].pop();
+ return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
+ };
+
+ ContributorsAuthorGraph.prototype.draw_path = function(data) {
+ return this.svg.append("path").datum(data).attr("class", "area-contributor").attr("d", this.area);
+ };
+
+ ContributorsAuthorGraph.prototype.draw = function() {
+ this.create_scale();
+ this.create_axes();
+ this.set_domain();
+ this.create_area(this.x, this.y);
+ this.create_svg();
+ this.draw_path(this.dates);
+ this.draw_x_axis();
+ return this.draw_y_axis();
+ };
+
+ ContributorsAuthorGraph.prototype.redraw = function() {
+ this.set_domain();
+ this.svg.select("path").datum(this.dates);
+ this.svg.select("path").attr("d", this.area);
+ this.svg.select(".x.axis").call(this.x_axis);
+ return this.svg.select(".y.axis").call(this.y_axis);
+ };
+
+ return ContributorsAuthorGraph;
+
+ })(ContributorsGraph);
+
+}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
deleted file mode 100644
index 834a81af459..00000000000
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
+++ /dev/null
@@ -1,173 +0,0 @@
-#= require d3
-
-class @ContributorsGraph
- MARGIN:
- top: 20
- right: 20
- bottom: 30
- left: 50
- x_domain: null
- y_domain: null
- dates: []
- @set_x_domain: (data) =>
- @prototype.x_domain = data
- @set_y_domain: (data) =>
- @prototype.y_domain = [0, d3.max(data, (d) ->
- d.commits = d.commits ? d.additions ? d.deletions
- )]
- @init_x_domain: (data) =>
- @prototype.x_domain = d3.extent(data, (d) ->
- d.date
- )
- @init_y_domain: (data) =>
- @prototype.y_domain = [0, d3.max(data, (d) ->
- d.commits = d.commits ? d.additions ? d.deletions
- )]
- @init_domain: (data) =>
- @init_x_domain(data)
- @init_y_domain(data)
- @set_dates: (data) =>
- @prototype.dates = data
- set_x_domain: ->
- @x.domain(@x_domain)
- set_y_domain: ->
- @y.domain(@y_domain)
- set_domain: ->
- @set_x_domain()
- @set_y_domain()
- create_scale: (width, height) ->
- @x = d3.time.scale().range([0, width]).clamp(true)
- @y = d3.scale.linear().range([height, 0]).nice()
- draw_x_axis: ->
- @svg.append("g").attr("class", "x axis").attr("transform", "translate(0, #{@height})")
- .call(@x_axis)
- draw_y_axis: ->
- @svg.append("g").attr("class", "y axis").call(@y_axis)
- set_data: (data) ->
- @data = data
-
-class @ContributorsMasterGraph extends ContributorsGraph
- constructor: (@data) ->
- @width = $('.content').width() - 70
- @height = 200
- @x = null
- @y = null
- @x_axis = null
- @y_axis = null
- @area = null
- @svg = null
- @brush = null
- @x_max_domain = null
- process_dates: (data) ->
- dates = @get_dates(data)
- @parse_dates(data)
- ContributorsGraph.set_dates(dates)
- get_dates: (data) ->
- _.pluck(data, 'date')
- parse_dates: (data) ->
- parseDate = d3.time.format("%Y-%m-%d").parse
- data.forEach((d) ->
- d.date = parseDate(d.date)
- )
- create_scale: ->
- super @width, @height
- create_axes: ->
- @x_axis = d3.svg.axis().scale(@x).orient("bottom")
- @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5)
- create_svg: ->
- @svg = d3.select("#contributors-master").append("svg")
- .attr("width", @width + @MARGIN.left + @MARGIN.right)
- .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
- .attr("class", "tint-box")
- .append("g")
- .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
- create_area: (x, y) ->
- @area = d3.svg.area().x((d) ->
- x(d.date)
- ).y0(@height).y1((d) ->
- xa = d.commits = d.commits ? d.additions ? d.deletions
- y(xa)
- ).interpolate("basis")
- create_brush: ->
- @brush = d3.svg.brush().x(@x).on("brushend", @update_content)
- draw_path: (data) ->
- @svg.append("path").datum(data).attr("class", "area").attr("d", @area)
- add_brush: ->
- @svg.append("g").attr("class", "selection").call(@brush).selectAll("rect").attr("height", @height)
- update_content: =>
- ContributorsGraph.set_x_domain(if @brush.empty() then @x_max_domain else @brush.extent())
- $("#brush_change").trigger('change')
- draw: ->
- @process_dates(@data)
- @create_scale()
- @create_axes()
- ContributorsGraph.init_domain(@data)
- @x_max_domain = @x_domain
- @set_domain()
- @create_area(@x, @y)
- @create_svg()
- @create_brush()
- @draw_path(@data)
- @draw_x_axis()
- @draw_y_axis()
- @add_brush()
- redraw: ->
- @process_dates(@data)
- ContributorsGraph.set_y_domain(@data)
- @set_y_domain()
- @svg.select("path").datum(@data)
- @svg.select("path").attr("d", @area)
- @svg.select(".y.axis").call(@y_axis)
-
-class @ContributorsAuthorGraph extends ContributorsGraph
- constructor: (@data) ->
- # Don't split graph size in half for mobile devices.
- if $(window).width() < 768
- @width = $('.content').width() - 80
- else
- @width = ($('.content').width() / 2) - 100
- @height = 200
- @x = null
- @y = null
- @x_axis = null
- @y_axis = null
- @area = null
- @svg = null
- @list_item = null
- create_scale: ->
- super @width, @height
- create_axes: ->
- @x_axis = d3.svg.axis().scale(@x).orient("bottom").ticks(8)
- @y_axis = d3.svg.axis().scale(@y).orient("left").ticks(5)
- create_area: (x, y) ->
- @area = d3.svg.area().x((d) ->
- parseDate = d3.time.format("%Y-%m-%d").parse
- x(parseDate(d))
- ).y0(@height).y1((d) =>
- if @data[d]? then y(@data[d]) else y(0)
- ).interpolate("basis")
- create_svg: ->
- @list_item = d3.selectAll(".person")[0].pop()
- @svg = d3.select(@list_item).append("svg")
- .attr("width", @width + @MARGIN.left + @MARGIN.right)
- .attr("height", @height + @MARGIN.top + @MARGIN.bottom)
- .attr("class", "spark")
- .append("g")
- .attr("transform", "translate(" + @MARGIN.left + "," + @MARGIN.top + ")")
- draw_path: (data) ->
- @svg.append("path").datum(data).attr("class", "area-contributor").attr("d", @area)
- draw: ->
- @create_scale()
- @create_axes()
- @set_domain()
- @create_area(@x, @y)
- @create_svg()
- @draw_path(@dates)
- @draw_x_axis()
- @draw_y_axis()
- redraw: ->
- @set_domain()
- @svg.select("path").datum(@dates)
- @svg.select("path").attr("d", @area)
- @svg.select(".x.axis").call(@x_axis)
- @svg.select(".y.axis").call(@y_axis)
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
new file mode 100644
index 00000000000..0d240bed8b6
--- /dev/null
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -0,0 +1,135 @@
+(function() {
+ window.ContributorsStatGraphUtil = {
+ parse_log: function(log) {
+ var by_author, by_email, data, entry, i, len, total;
+ total = {};
+ by_author = {};
+ by_email = {};
+ for (i = 0, len = log.length; i < len; i++) {
+ entry = log[i];
+ if (total[entry.date] == null) {
+ this.add_date(entry.date, total);
+ }
+ data = by_author[entry.author_name] || by_email[entry.author_email];
+ if (data == null) {
+ data = this.add_author(entry, by_author, by_email);
+ }
+ if (!data[entry.date]) {
+ this.add_date(entry.date, data);
+ }
+ this.store_data(entry, total[entry.date], data[entry.date]);
+ }
+ total = _.toArray(total);
+ by_author = _.toArray(by_author);
+ return {
+ total: total,
+ by_author: by_author
+ };
+ },
+ add_date: function(date, collection) {
+ collection[date] = {};
+ return collection[date].date = date;
+ },
+ add_author: function(author, by_author, by_email) {
+ var data;
+ data = {};
+ data.author_name = author.author_name;
+ data.author_email = author.author_email;
+ by_author[author.author_name] = data;
+ return by_email[author.author_email] = data;
+ },
+ store_data: function(entry, total, by_author) {
+ this.store_commits(total, by_author);
+ this.store_additions(entry, total, by_author);
+ return this.store_deletions(entry, total, by_author);
+ },
+ store_commits: function(total, by_author) {
+ this.add(total, "commits", 1);
+ return this.add(by_author, "commits", 1);
+ },
+ add: function(collection, field, value) {
+ if (collection[field] == null) {
+ collection[field] = 0;
+ }
+ return collection[field] += value;
+ },
+ store_additions: function(entry, total, by_author) {
+ if (entry.additions == null) {
+ entry.additions = 0;
+ }
+ this.add(total, "additions", entry.additions);
+ return this.add(by_author, "additions", entry.additions);
+ },
+ store_deletions: function(entry, total, by_author) {
+ if (entry.deletions == null) {
+ entry.deletions = 0;
+ }
+ this.add(total, "deletions", entry.deletions);
+ return this.add(by_author, "deletions", entry.deletions);
+ },
+ get_total_data: function(parsed_log, field) {
+ var log, total_data;
+ log = parsed_log.total;
+ total_data = this.pick_field(log, field);
+ return _.sortBy(total_data, function(d) {
+ return d.date;
+ });
+ },
+ pick_field: function(log, field) {
+ var total_data;
+ total_data = [];
+ _.each(log, function(d) {
+ return total_data.push(_.pick(d, [field, 'date']));
+ });
+ return total_data;
+ },
+ get_author_data: function(parsed_log, field, date_range) {
+ var author_data, log;
+ if (date_range == null) {
+ date_range = null;
+ }
+ log = parsed_log.by_author;
+ author_data = [];
+ _.each(log, (function(_this) {
+ return function(log_entry) {
+ var parsed_log_entry;
+ parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
+ if (!_.isEmpty(parsed_log_entry.dates)) {
+ return author_data.push(parsed_log_entry);
+ }
+ };
+ })(this));
+ return _.sortBy(author_data, function(d) {
+ return d[field];
+ }).reverse();
+ },
+ parse_log_entry: function(log_entry, field, date_range) {
+ var parsed_entry;
+ parsed_entry = {};
+ parsed_entry.author_name = log_entry.author_name;
+ parsed_entry.author_email = log_entry.author_email;
+ parsed_entry.dates = {};
+ parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0;
+ _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) {
+ return function(value, key) {
+ if (_this.in_range(value.date, date_range)) {
+ parsed_entry.dates[value.date] = value[field];
+ parsed_entry.commits += value.commits;
+ parsed_entry.additions += value.additions;
+ return parsed_entry.deletions += value.deletions;
+ }
+ };
+ })(this));
+ return parsed_entry;
+ },
+ in_range: function(date, date_range) {
+ var ref;
+ if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
deleted file mode 100644
index 31617c88b4a..00000000000
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js.coffee
+++ /dev/null
@@ -1,98 +0,0 @@
-window.ContributorsStatGraphUtil =
- parse_log: (log) ->
- total = {}
- by_author = {}
- by_email = {}
- for entry in log
- @add_date(entry.date, total) unless total[entry.date]?
-
- data = by_author[entry.author_name] || by_email[entry.author_email]
- data ?= @add_author(entry, by_author, by_email)
-
- @add_date(entry.date, data) unless data[entry.date]
- @store_data(entry, total[entry.date], data[entry.date])
- total = _.toArray(total)
- by_author = _.toArray(by_author)
- total: total, by_author: by_author
-
- add_date: (date, collection) ->
- collection[date] = {}
- collection[date].date = date
-
- add_author: (author, by_author, by_email) ->
- data = {}
- data.author_name = author.author_name
- data.author_email = author.author_email
- by_author[author.author_name] = data
- by_email[author.author_email] = data
-
- store_data: (entry, total, by_author) ->
- @store_commits(total, by_author)
- @store_additions(entry, total, by_author)
- @store_deletions(entry, total, by_author)
-
- store_commits: (total, by_author) ->
- @add(total, "commits", 1)
- @add(by_author, "commits", 1)
-
- add: (collection, field, value) ->
- collection[field] ?= 0
- collection[field] += value
-
- store_additions: (entry, total, by_author) ->
- entry.additions ?= 0
- @add(total, "additions", entry.additions)
- @add(by_author, "additions", entry.additions)
-
- store_deletions: (entry, total, by_author) ->
- entry.deletions ?= 0
- @add(total, "deletions", entry.deletions)
- @add(by_author, "deletions", entry.deletions)
-
- get_total_data: (parsed_log, field) ->
- log = parsed_log.total
- total_data = @pick_field(log, field)
- _.sortBy(total_data, (d) ->
- d.date
- )
- pick_field: (log, field) ->
- total_data = []
- _.each(log, (d) ->
- total_data.push(_.pick(d, [field, 'date']))
- )
- total_data
-
- get_author_data: (parsed_log, field, date_range = null) ->
- log = parsed_log.by_author
- author_data = []
-
- _.each(log, (log_entry) =>
- parsed_log_entry = @parse_log_entry(log_entry, field, date_range)
- if not _.isEmpty(parsed_log_entry.dates)
- author_data.push(parsed_log_entry)
- )
-
- _.sortBy(author_data, (d) ->
- d[field]
- ).reverse()
-
- parse_log_entry: (log_entry, field, date_range) ->
- parsed_entry = {}
- parsed_entry.author_name = log_entry.author_name
- parsed_entry.author_email = log_entry.author_email
- parsed_entry.dates = {}
- parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0
- _.each(_.omit(log_entry, 'author_name', 'author_email'), (value, key) =>
- if @in_range(value.date, date_range)
- parsed_entry.dates[value.date] = value[field]
- parsed_entry.commits += value.commits
- parsed_entry.additions += value.additions
- parsed_entry.deletions += value.deletions
- )
- return parsed_entry
-
- in_range: (date, date_range) ->
- if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
- true
- else
- false
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
new file mode 100644
index 00000000000..c28ce86d7af
--- /dev/null
+++ b/app/assets/javascripts/group_avatar.js
@@ -0,0 +1,21 @@
+(function() {
+ this.GroupAvatar = (function() {
+ function GroupAvatar() {
+ $('.js-choose-group-avatar-button').bind("click", function() {
+ var form;
+ form = $(this).closest("form");
+ return form.find(".js-group-avatar-input").click();
+ });
+ $('.js-group-avatar-input').bind("change", function() {
+ var filename, form;
+ form = $(this).closest("form");
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find(".js-avatar-filename").text(filename);
+ });
+ }
+
+ return GroupAvatar;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/group_avatar.js.coffee b/app/assets/javascripts/group_avatar.js.coffee
deleted file mode 100644
index 0825fd3ce52..00000000000
--- a/app/assets/javascripts/group_avatar.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-class @GroupAvatar
- constructor: ->
- $('.js-choose-group-avatar-button').bind "click", ->
- form = $(this).closest("form")
- form.find(".js-group-avatar-input").click()
- $('.js-group-avatar-input').bind "change", ->
- form = $(this).closest("form")
- filename = $(this).val().replace(/^.*[\\\/]/, '')
- form.find(".js-avatar-filename").text(filename)
diff --git a/app/assets/javascripts/groups.js b/app/assets/javascripts/groups.js
new file mode 100644
index 00000000000..4382dd6860f
--- /dev/null
+++ b/app/assets/javascripts/groups.js
@@ -0,0 +1,13 @@
+(function() {
+ this.GroupMembers = (function() {
+ function GroupMembers() {
+ $('li.group_member').bind('ajax:success', function() {
+ return $(this).fadeOut();
+ });
+ }
+
+ return GroupMembers;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/groups.js.coffee b/app/assets/javascripts/groups.js.coffee
deleted file mode 100644
index cc905e91ea2..00000000000
--- a/app/assets/javascripts/groups.js.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-class @GroupMembers
- constructor: ->
- $('li.group_member').bind 'ajax:success', ->
- $(this).fadeOut()
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
new file mode 100644
index 00000000000..fd5b6dc0ddd
--- /dev/null
+++ b/app/assets/javascripts/groups_select.js
@@ -0,0 +1,67 @@
+(function() {
+ var slice = [].slice;
+
+ this.GroupsSelect = (function() {
+ function GroupsSelect() {
+ $('.ajax-groups-select').each((function(_this) {
+ return function(i, select) {
+ var skip_ldap;
+ skip_ldap = $(select).hasClass('skip_ldap');
+ return $(select).select2({
+ placeholder: "Search for a group",
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return Api.groups(query.term, skip_ldap, function(groups) {
+ var data;
+ data = {
+ results: groups
+ };
+ return query.callback(data);
+ });
+ },
+ initSelection: function(element, callback) {
+ var id;
+ id = $(element).val();
+ if (id !== "") {
+ return Api.group(id, callback);
+ }
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: "ajax-groups-dropdown",
+ escapeMarkup: function(m) {
+ return m;
+ }
+ });
+ };
+ })(this));
+ }
+
+ GroupsSelect.prototype.formatResult = function(group) {
+ var avatar;
+ if (group.avatar_url) {
+ avatar = group.avatar_url;
+ } else {
+ avatar = gon.default_avatar_url;
+ }
+ return "<div class='group-result'> <div class='group-name'>" + group.name + "</div> <div class='group-path'>" + group.path + "</div> </div>";
+ };
+
+ GroupsSelect.prototype.formatSelection = function(group) {
+ return group.name;
+ };
+
+ return GroupsSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/groups_select.js.coffee b/app/assets/javascripts/groups_select.js.coffee
deleted file mode 100644
index 1084e2a17d1..00000000000
--- a/app/assets/javascripts/groups_select.js.coffee
+++ /dev/null
@@ -1,41 +0,0 @@
-class @GroupsSelect
- constructor: ->
- $('.ajax-groups-select').each (i, select) =>
- skip_ldap = $(select).hasClass('skip_ldap')
-
- $(select).select2
- placeholder: "Search for a group"
- multiple: $(select).hasClass('multiselect')
- minimumInputLength: 0
- query: (query) ->
- Api.groups query.term, skip_ldap, (groups) ->
- data = { results: groups }
- query.callback(data)
-
- initSelection: (element, callback) ->
- id = $(element).val()
- if id isnt ""
- Api.group(id, callback)
-
-
- formatResult: (args...) =>
- @formatResult(args...)
- formatSelection: (args...) =>
- @formatSelection(args...)
- dropdownCssClass: "ajax-groups-dropdown"
- escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
- m
-
- formatResult: (group) ->
- if group.avatar_url
- avatar = group.avatar_url
- else
- avatar = gon.default_avatar_url
-
- "<div class='group-result'>
- <div class='group-name'>#{group.name}</div>
- <div class='group-path'>#{group.path}</div>
- </div>"
-
- formatSelection: (group) ->
- group.name
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
new file mode 100644
index 00000000000..0f840821f53
--- /dev/null
+++ b/app/assets/javascripts/importer_status.js
@@ -0,0 +1,77 @@
+(function() {
+ this.ImporterStatus = (function() {
+ function ImporterStatus(jobs_url, import_url) {
+ this.jobs_url = jobs_url;
+ this.import_url = import_url;
+ this.initStatusPage();
+ this.setAutoUpdate();
+ }
+
+ ImporterStatus.prototype.initStatusPage = function() {
+ $('.js-add-to-import').off('click').on('click', (function(_this) {
+ return function(e) {
+ var $btn, $namespace_input, $target_field, $tr, id, new_namespace;
+ $btn = $(e.currentTarget);
+ $tr = $btn.closest('tr');
+ $target_field = $tr.find('.import-target');
+ $namespace_input = $target_field.find('input');
+ id = $tr.attr('id').replace('repo_', '');
+ new_namespace = null;
+ if ($namespace_input.length > 0) {
+ new_namespace = $namespace_input.prop('value');
+ $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name')));
+ }
+ $btn.disable().addClass('is-loading');
+ return $.post(_this.import_url, {
+ repo_id: id,
+ new_namespace: new_namespace
+ }, {
+ dataType: 'script'
+ });
+ };
+ })(this));
+ return $('.js-import-all').off('click').on('click', function(e) {
+ var $btn;
+ $btn = $(this);
+ $btn.disable().addClass('is-loading');
+ return $('.js-add-to-import').each(function() {
+ return $(this).trigger('click');
+ });
+ });
+ };
+
+ ImporterStatus.prototype.setAutoUpdate = function() {
+ return setInterval(((function(_this) {
+ return function() {
+ return $.get(_this.jobs_url, function(data) {
+ return $.each(data, function(i, job) {
+ var job_item, status_field;
+ job_item = $("#project_" + job.id);
+ status_field = job_item.find(".job-status");
+ if (job.import_status === 'finished') {
+ job_item.removeClass("active").addClass("success");
+ return status_field.html('<span><i class="fa fa-check"></i> done</span>');
+ } else if (job.import_status === 'started') {
+ return status_field.html("<i class='fa fa-spinner fa-spin'></i> started");
+ } else {
+ return status_field.html(job.import_status);
+ }
+ });
+ });
+ };
+ })(this)), 4000);
+ };
+
+ return ImporterStatus;
+
+ })();
+
+ $(function() {
+ if ($('.js-importer-status').length) {
+ var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
+ var importPath = $('.js-importer-status').data('import-path');
+
+ new ImporterStatus(jobsImportPath, importPath);
+ }
+ });
+}).call(this);
diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee
deleted file mode 100644
index eb046eb2eff..00000000000
--- a/app/assets/javascripts/importer_status.js.coffee
+++ /dev/null
@@ -1,53 +0,0 @@
-class @ImporterStatus
- constructor: (@jobs_url, @import_url) ->
- this.initStatusPage()
- this.setAutoUpdate()
-
- initStatusPage: ->
- $('.js-add-to-import')
- .off 'click'
- .on 'click', (e) =>
- $btn = $(e.currentTarget)
- $tr = $btn.closest('tr')
- $target_field = $tr.find('.import-target')
- $namespace_input = $target_field.find('input')
- id = $tr.attr('id').replace('repo_', '')
- new_namespace = null
-
- if $namespace_input.length > 0
- new_namespace = $namespace_input.prop('value')
- $target_field.empty().append("#{new_namespace}/#{$target_field.data('project_name')}")
-
- $btn
- .disable()
- .addClass 'is-loading'
-
- $.post @import_url, {repo_id: id, new_namespace: new_namespace}, dataType: 'script'
-
- $('.js-import-all')
- .off 'click'
- .on 'click', (e) ->
- $btn = $(@)
- $btn
- .disable()
- .addClass 'is-loading'
-
- $('.js-add-to-import').each ->
- $(this).trigger('click')
-
- setAutoUpdate: ->
- setInterval (=>
- $.get @jobs_url, (data) =>
- $.each data, (i, job) =>
- job_item = $("#project_" + job.id)
- status_field = job_item.find(".job-status")
-
- if job.import_status == 'finished'
- job_item.removeClass("active").addClass("success")
- status_field.html('<span><i class="fa fa-check"></i> done</span>')
- else if job.import_status == 'started'
- status_field.html("<i class='fa fa-spinner fa-spin'></i> started")
- else
- status_field.html(job.import_status)
-
- ), 4000
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js
new file mode 100644
index 00000000000..d0305c6c6a1
--- /dev/null
+++ b/app/assets/javascripts/issuable.js
@@ -0,0 +1,86 @@
+(function() {
+ var issuable_created;
+
+ issuable_created = false;
+
+ this.Issuable = {
+ init: function() {
+ Issuable.initTemplates();
+ Issuable.initSearch();
+ Issuable.initChecks();
+ return Issuable.initLabelFilterRemove();
+ },
+ initTemplates: function() {
+ return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
+ },
+ initSearch: function() {
+ this.timer = null;
+ return $('#issue_search').off('keyup').on('keyup', function() {
+ clearTimeout(this.timer);
+ return this.timer = setTimeout(function() {
+ var $form, $input, $search;
+ $search = $('#issue_search');
+ $form = $('.js-filter-form');
+ $input = $("input[name='" + ($search.attr('name')) + "']", $form);
+ if ($input.length === 0) {
+ $form.append("<input type='hidden' name='" + ($search.attr('name')) + "' value='" + (_.escape($search.val())) + "'/>");
+ } else {
+ $input.val($search.val());
+ }
+ if ($search.val() !== '') {
+ return Issuable.filterResults($form);
+ }
+ }, 500);
+ });
+ },
+ initLabelFilterRemove: function() {
+ return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
+ var $button;
+ $button = $(this);
+ $('input[name="label_name[]"]').filter(function() {
+ return this.value === $button.data('label');
+ }).remove();
+ Issuable.filterResults($('.filter-form'));
+ return $('.js-label-select').trigger('update.label');
+ });
+ },
+ filterResults: (function(_this) {
+ return function(form) {
+ var formAction, formData, issuesUrl;
+ formData = form.serialize();
+ formAction = form.attr('action');
+ issuesUrl = formAction;
+ issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
+ issuesUrl += formData;
+ return Turbolinks.visit(issuesUrl);
+ };
+ })(this),
+ initChecks: function() {
+ this.issuableBulkActions = $('.bulk-update').data('bulkActions');
+ $('.check_all_issues').off('click').on('click', function() {
+ $('.selected_issue').prop('checked', this.checked);
+ return Issuable.checkChanged();
+ });
+ return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
+ },
+ checkChanged: function() {
+ var checked_issues, ids;
+ checked_issues = $('.selected_issue:checked');
+ if (checked_issues.length > 0) {
+ ids = $.map(checked_issues, function(value) {
+ return $(value).data('id');
+ });
+ $('#update_issues_ids').val(ids);
+ $('.issues-other-filters').hide();
+ $('.issues_bulk_update').show();
+ } else {
+ $('#update_issues_ids').val([]);
+ $('.issues_bulk_update').hide();
+ $('.issues-other-filters').show();
+ this.issuableBulkActions.willUpdateLabels = false;
+ }
+ return true;
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee
deleted file mode 100644
index c71d4ecf505..00000000000
--- a/app/assets/javascripts/issuable.js.coffee
+++ /dev/null
@@ -1,95 +0,0 @@
-issuable_created = false
-@Issuable =
- init: ->
- unless issuable_created
- issuable_created = true
- Issuable.initTemplates()
- Issuable.initSearch()
- Issuable.initChecks()
- Issuable.initLabelFilterRemove()
-
- initTemplates: ->
- Issuable.labelRow = _.template(
- '<% _.each(labels, function(label){ %>
- <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;">
- <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body">
- <%- label.title %>
- </a>
- <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>">
- <i class="fa fa-times"></i>
- </button>
- </span>
- <% }); %>'
- )
-
- initSearch: ->
- @timer = null
- $('#issue_search')
- .off 'keyup'
- .on 'keyup', ->
- clearTimeout(@timer)
- @timer = setTimeout( ->
- $search = $('#issue_search')
- $form = $('.js-filter-form')
- $input = $("input[name='#{$search.attr('name')}']", $form)
-
- if $input.length is 0
- $form.append "<input type='hidden' name='#{$search.attr('name')}' value='#{_.escape($search.val())}'/>"
- else
- $input.val $search.val()
-
- Issuable.filterResults $form
- , 500)
-
- initLabelFilterRemove: ->
- $(document)
- .off 'click', '.js-label-filter-remove'
- .on 'click', '.js-label-filter-remove', (e) ->
- $button = $(@)
-
- # Remove the label input box
- $('input[name="label_name[]"]')
- .filter -> @value is $button.data('label')
- .remove()
-
- # Submit the form to get new data
- Issuable.filterResults $('.filter-form')
- $('.js-label-select').trigger('update.label')
-
- filterResults: (form) =>
- formData = form.serialize()
-
- formAction = form.attr('action')
- issuesUrl = formAction
- issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
- issuesUrl += formData
-
- Turbolinks.visit(issuesUrl)
-
- initChecks: ->
- @issuableBulkActions = $('.bulk-update').data('bulkActions')
-
- $('.check_all_issues').off('click').on('click', ->
- $('.selected_issue').prop('checked', @checked)
- Issuable.checkChanged()
- )
-
- $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(@))
-
-
- checkChanged: ->
- checked_issues = $('.selected_issue:checked')
- if checked_issues.length > 0
- ids = $.map checked_issues, (value) ->
- $(value).data('id')
-
- $('#update_issues_ids').val ids
- $('.issues-other-filters').hide()
- $('.issues_bulk_update').show()
- else
- $('#update_issues_ids').val []
- $('.issues_bulk_update').hide()
- $('.issues-other-filters').show()
- @issuableBulkActions.willUpdateLabels = false
-
- return true
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
new file mode 100644
index 00000000000..8147e83ffe8
--- /dev/null
+++ b/app/assets/javascripts/issuable_context.js
@@ -0,0 +1,69 @@
+(function() {
+ this.IssuableContext = (function() {
+ function IssuableContext(currentUser) {
+ this.initParticipants();
+ new UsersSelect(currentUser);
+ $('select.select2').select2({
+ width: 'resolve',
+ dropdownAutoWidth: true
+ });
+ $(".issuable-sidebar .inline-update").on("change", "select", function() {
+ return $(this).submit();
+ });
+ $(".issuable-sidebar .inline-update").on("change", ".js-assignee", function() {
+ return $(this).submit();
+ });
+ $(document).off('click', '.issuable-sidebar .dropdown-content a').on('click', '.issuable-sidebar .dropdown-content a', function(e) {
+ return e.preventDefault();
+ });
+ $(document).off('click', '.edit-link').on('click', '.edit-link', function(e) {
+ var $block, $selectbox;
+ e.preventDefault();
+ $block = $(this).parents('.block');
+ $selectbox = $block.find('.selectbox');
+ if ($selectbox.is(':visible')) {
+ $selectbox.hide();
+ $block.find('.value').show();
+ } else {
+ $selectbox.show();
+ $block.find('.value').hide();
+ }
+ if ($selectbox.is(':visible')) {
+ return setTimeout(function() {
+ return $block.find('.dropdown-menu-toggle').trigger('click');
+ }, 0);
+ }
+ });
+ $(".right-sidebar").niceScroll();
+ }
+
+ IssuableContext.prototype.initParticipants = function() {
+ var _this;
+ _this = this;
+ $(document).on("click", ".js-participants-more", this.toggleHiddenParticipants);
+ return $(".js-participants-author").each(function(i) {
+ if (i >= _this.PARTICIPANTS_ROW_COUNT) {
+ return $(this).addClass("js-participants-hidden").hide();
+ }
+ });
+ };
+
+ IssuableContext.prototype.toggleHiddenParticipants = function(e) {
+ var currentText, lessText, originalText;
+ e.preventDefault();
+ currentText = $(this).text().trim();
+ lessText = $(this).data("less-text");
+ originalText = $(this).data("original-text");
+ if (currentText === originalText) {
+ $(this).text(lessText);
+ } else {
+ $(this).text(originalText);
+ }
+ return $(".js-participants-hidden").toggle();
+ };
+
+ return IssuableContext;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
deleted file mode 100644
index 3c491ebfc4c..00000000000
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ /dev/null
@@ -1,60 +0,0 @@
-class @IssuableContext
- constructor: (currentUser) ->
- @initParticipants()
- new UsersSelect(currentUser)
- $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
-
- $(".issuable-sidebar .inline-update").on "change", "select", ->
- $(this).submit()
- $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
- $(this).submit()
-
- $(document)
- .off 'click', '.issuable-sidebar .dropdown-content a'
- .on 'click', '.issuable-sidebar .dropdown-content a', (e) ->
- e.preventDefault()
-
- $(document)
- .off 'click', '.edit-link'
- .on 'click', '.edit-link', (e) ->
- e.preventDefault()
-
- $block = $(@).parents('.block')
- $selectbox = $block.find('.selectbox')
- if $selectbox.is(':visible')
- $selectbox.hide()
- $block.find('.value').show()
- else
- $selectbox.show()
- $block.find('.value').hide()
-
- if $selectbox.is(':visible')
- setTimeout ->
- $block.find('.dropdown-menu-toggle').trigger 'click'
- , 0
-
- $(".right-sidebar").niceScroll()
-
- initParticipants: ->
- _this = @
- $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
-
- $(".js-participants-author").each (i) ->
- if i >= _this.PARTICIPANTS_ROW_COUNT
- $(@)
- .addClass "js-participants-hidden"
- .hide()
-
- toggleHiddenParticipants: (e) ->
- e.preventDefault()
-
- currentText = $(this).text().trim()
- lessText = $(this).data("less-text")
- originalText = $(this).data("original-text")
-
- if currentText is originalText
- $(this).text(lessText)
- else
- $(this).text(originalText)
-
- $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
new file mode 100644
index 00000000000..b7f92ae9883
--- /dev/null
+++ b/app/assets/javascripts/issuable_form.js
@@ -0,0 +1,150 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.IssuableForm = (function() {
+ IssuableForm.prototype.issueMoveConfirmMsg = 'Are you sure you want to move this issue to another project?';
+
+ IssuableForm.prototype.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
+
+ function IssuableForm(form) {
+ var $issuableDueDate;
+ this.form = form;
+ this.toggleWip = bind(this.toggleWip, this);
+ this.renderWipExplanation = bind(this.renderWipExplanation, this);
+ this.resetAutosave = bind(this.resetAutosave, this);
+ this.handleSubmit = bind(this.handleSubmit, this);
+ GitLab.GfmAutoComplete.setup();
+ new UsersSelect();
+ new ZenMode();
+ this.titleField = this.form.find("input[name*='[title]']");
+ this.descriptionField = this.form.find("textarea[name*='[description]']");
+ this.issueMoveField = this.form.find("#move_to_project_id");
+ if (!(this.titleField.length && this.descriptionField.length)) {
+ return;
+ }
+ this.initAutosave();
+ this.form.on("submit", this.handleSubmit);
+ this.form.on("click", ".btn-cancel", this.resetAutosave);
+ this.initWip();
+ this.initMoveDropdown();
+ $issuableDueDate = $('#issuable-due-date');
+ if ($issuableDueDate.length) {
+ $('.datepicker').datepicker({
+ dateFormat: 'yy-mm-dd',
+ onSelect: function(dateText, inst) {
+ return $issuableDueDate.val(dateText);
+ }
+ }).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val()));
+ }
+ }
+
+ IssuableForm.prototype.initAutosave = function() {
+ new Autosave(this.titleField, [document.location.pathname, document.location.search, "title"]);
+ return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, "description"]);
+ };
+
+ IssuableForm.prototype.handleSubmit = function() {
+ var ref, ref1;
+ if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) {
+ if (!confirm(this.issueMoveConfirmMsg)) {
+ return false;
+ }
+ }
+ return this.resetAutosave();
+ };
+
+ IssuableForm.prototype.resetAutosave = function() {
+ this.titleField.data("autosave").reset();
+ return this.descriptionField.data("autosave").reset();
+ };
+
+ IssuableForm.prototype.initWip = function() {
+ this.$wipExplanation = this.form.find(".js-wip-explanation");
+ this.$noWipExplanation = this.form.find(".js-no-wip-explanation");
+ if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
+ return;
+ }
+ this.form.on("click", ".js-toggle-wip", this.toggleWip);
+ this.titleField.on("keyup blur", this.renderWipExplanation);
+ return this.renderWipExplanation();
+ };
+
+ IssuableForm.prototype.workInProgress = function() {
+ return this.wipRegex.test(this.titleField.val());
+ };
+
+ IssuableForm.prototype.renderWipExplanation = function() {
+ if (this.workInProgress()) {
+ this.$wipExplanation.show();
+ return this.$noWipExplanation.hide();
+ } else {
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
+ }
+ };
+
+ IssuableForm.prototype.toggleWip = function(event) {
+ event.preventDefault();
+ if (this.workInProgress()) {
+ this.removeWip();
+ } else {
+ this.addWip();
+ }
+ return this.renderWipExplanation();
+ };
+
+ IssuableForm.prototype.removeWip = function() {
+ return this.titleField.val(this.titleField.val().replace(this.wipRegex, ""));
+ };
+
+ IssuableForm.prototype.addWip = function() {
+ return this.titleField.val("WIP: " + (this.titleField.val()));
+ };
+
+ IssuableForm.prototype.initMoveDropdown = function() {
+ var $moveDropdown, pageSize;
+ $moveDropdown = $('.js-move-dropdown');
+ if ($moveDropdown.length) {
+ pageSize = $moveDropdown.data('page-size');
+ return $('.js-move-dropdown').select2({
+ ajax: {
+ url: $moveDropdown.data('projects-url'),
+ quietMillis: 125,
+ data: function(term, page, context) {
+ return {
+ search: term,
+ offset_id: context
+ };
+ },
+ results: function(data) {
+ var context,
+ more;
+
+ if (data.length >= pageSize)
+ more = true;
+
+ if (data[data.length - 1])
+ context = data[data.length - 1].id;
+
+ return {
+ results: data,
+ more: more,
+ context: context
+ };
+ }
+ },
+ formatResult: function(project) {
+ return project.name_with_namespace;
+ },
+ formatSelection: function(project) {
+ return project.name_with_namespace;
+ }
+ });
+ }
+ };
+
+ return IssuableForm;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/issuable_form.js.coffee b/app/assets/javascripts/issuable_form.js.coffee
deleted file mode 100644
index 5b7a4831dfc..00000000000
--- a/app/assets/javascripts/issuable_form.js.coffee
+++ /dev/null
@@ -1,112 +0,0 @@
-class @IssuableForm
- issueMoveConfirmMsg: 'Are you sure you want to move this issue to another project?'
- wipRegex: /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i
-
- constructor: (@form) ->
- GitLab.GfmAutoComplete.setup()
- new UsersSelect()
- new ZenMode()
-
- @titleField = @form.find("input[name*='[title]']")
- @descriptionField = @form.find("textarea[name*='[description]']")
- @issueMoveField = @form.find("#move_to_project_id")
-
- return unless @titleField.length && @descriptionField.length
-
- @initAutosave()
-
- @form.on "submit", @handleSubmit
- @form.on "click", ".btn-cancel", @resetAutosave
-
- @initWip()
- @initMoveDropdown()
-
- $issuableDueDate = $('#issuable-due-date')
-
- if $issuableDueDate.length
- $('.datepicker').datepicker(
- dateFormat: 'yy-mm-dd',
- onSelect: (dateText, inst) ->
- $issuableDueDate.val dateText
- ).datepicker 'setDate', $.datepicker.parseDate('yy-mm-dd', $issuableDueDate.val())
-
- initAutosave: ->
- new Autosave @titleField, [
- document.location.pathname,
- document.location.search,
- "title"
- ]
-
- new Autosave @descriptionField, [
- document.location.pathname,
- document.location.search,
- "description"
- ]
-
- handleSubmit: =>
- if (parseInt(@issueMoveField?.val()) ? 0) > 0
- return false unless confirm(@issueMoveConfirmMsg)
-
- @resetAutosave()
-
- resetAutosave: =>
- @titleField.data("autosave").reset()
- @descriptionField.data("autosave").reset()
-
- initWip: ->
- @$wipExplanation = @form.find(".js-wip-explanation")
- @$noWipExplanation = @form.find(".js-no-wip-explanation")
- return unless @$wipExplanation.length and @$noWipExplanation.length
-
- @form.on "click", ".js-toggle-wip", @toggleWip
-
- @titleField.on "keyup blur", @renderWipExplanation
-
- @renderWipExplanation()
-
- workInProgress: ->
- @wipRegex.test @titleField.val()
-
- renderWipExplanation: =>
- if @workInProgress()
- @$wipExplanation.show()
- @$noWipExplanation.hide()
- else
- @$wipExplanation.hide()
- @$noWipExplanation.show()
-
- toggleWip: (event) =>
- event.preventDefault()
-
- if @workInProgress()
- @removeWip()
- else
- @addWip()
-
- @renderWipExplanation()
-
- removeWip: ->
- @titleField.val @titleField.val().replace(@wipRegex, "")
-
- addWip: ->
- @titleField.val "WIP: #{@titleField.val()}"
-
- initMoveDropdown: ->
- $moveDropdown = $('.js-move-dropdown')
-
- if $moveDropdown.length
- $('.js-move-dropdown').select2
- ajax:
- url: $moveDropdown.data('projects-url')
- results: (data) ->
- return {
- results: data
- }
- data: (query) ->
- {
- search: query
- }
- formatResult: (project) ->
- project.name_with_namespace
- formatSelection: (project) ->
- project.name_with_namespace
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
new file mode 100644
index 00000000000..6838d9d8da1
--- /dev/null
+++ b/app/assets/javascripts/issue.js
@@ -0,0 +1,154 @@
+
+/*= require flash */
+
+
+/*= require jquery.waitforimages */
+
+
+/*= require task_list */
+
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Issue = (function() {
+ function Issue() {
+ this.submitNoteForm = bind(this.submitNoteForm, this);
+ this.disableTaskList();
+ if ($('a.btn-close').length) {
+ this.initTaskList();
+ this.initIssueBtnEventListeners();
+ }
+ this.initMergeRequests();
+ this.initRelatedBranches();
+ this.initCanCreateBranch();
+ }
+
+ Issue.prototype.initTaskList = function() {
+ $('.detail-page-description .js-task-list-container').taskList('enable');
+ return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
+ };
+
+ Issue.prototype.initIssueBtnEventListeners = function() {
+ var _this, issueFailMessage;
+ _this = this;
+ issueFailMessage = 'Unable to update this issue at this time.';
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, isClose, shouldSubmit, url;
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $this = $(this);
+ isClose = $this.hasClass('btn-close');
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit) {
+ _this.submitNoteForm($this.closest('form'));
+ }
+ $this.prop('disabled', true);
+ url = $this.attr('href');
+ return $.ajax({
+ type: 'PUT',
+ url: url,
+ error: function(jqXHR, textStatus, errorThrown) {
+ var issueStatus;
+ issueStatus = isClose ? 'close' : 'open';
+ return new Flash(issueFailMessage, 'alert');
+ },
+ success: function(data, textStatus, jqXHR) {
+ if ('id' in data) {
+ $(document).trigger('issuable:change');
+ if (isClose) {
+ $('a.btn-close').addClass('hidden');
+ $('a.btn-reopen').removeClass('hidden');
+ $('div.status-box-closed').removeClass('hidden');
+ $('div.status-box-open').addClass('hidden');
+ } else {
+ $('a.btn-reopen').addClass('hidden');
+ $('a.btn-close').removeClass('hidden');
+ $('div.status-box-closed').addClass('hidden');
+ $('div.status-box-open').removeClass('hidden');
+ }
+ } else {
+ new Flash(issueFailMessage, 'alert');
+ }
+ return $this.prop('disabled', false);
+ }
+ });
+ });
+ };
+
+ Issue.prototype.submitNoteForm = function(form) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ return form.submit();
+ }
+ };
+
+ Issue.prototype.disableTaskList = function() {
+ $('.detail-page-description .js-task-list-container').taskList('disable');
+ return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
+ };
+
+ Issue.prototype.updateTaskList = function() {
+ var patchData;
+ patchData = {};
+ patchData['issue'] = {
+ 'description': $('.js-task-list-field', this).val()
+ };
+ return $.ajax({
+ type: 'PATCH',
+ url: $('form.js-issuable-update').attr('action'),
+ data: patchData
+ });
+ };
+
+ Issue.prototype.initMergeRequests = function() {
+ var $container;
+ $container = $('#merge-requests');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load referenced merge requests', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ };
+
+ Issue.prototype.initRelatedBranches = function() {
+ var $container;
+ $container = $('#related-branches');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load related branches', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ };
+
+ Issue.prototype.initCanCreateBranch = function() {
+ var $container;
+ $container = $('div#new-branch');
+ if ($container.length === 0) {
+ return;
+ }
+ return $.getJSON($container.data('path')).error(function() {
+ $container.find('.checking').hide();
+ $container.find('.unavailable').show();
+ return new Flash('Failed to check if a new branch can be created.', 'alert');
+ }).success(function(data) {
+ if (data.can_create_branch) {
+ $container.find('.checking').hide();
+ $container.find('.available').show();
+ return $container.find('a').attr('disabled', false);
+ } else {
+ $container.find('.checking').hide();
+ return $container.find('.unavailable').show();
+ }
+ });
+ };
+
+ return Issue;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
deleted file mode 100644
index f446aa49cde..00000000000
--- a/app/assets/javascripts/issue.js.coffee
+++ /dev/null
@@ -1,117 +0,0 @@
-#= require flash
-#= require jquery.waitforimages
-#= require task_list
-
-class @Issue
- constructor: ->
- # Prevent duplicate event bindings
- @disableTaskList()
- if $('a.btn-close').length
- @initTaskList()
- @initIssueBtnEventListeners()
-
- @initMergeRequests()
- @initRelatedBranches()
- @initCanCreateBranch()
-
- initTaskList: ->
- $('.detail-page-description .js-task-list-container').taskList('enable')
- $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
-
- initIssueBtnEventListeners: ->
- _this = @
- issueFailMessage = 'Unable to update this issue at this time.'
- $('a.btn-close, a.btn-reopen').on 'click', (e) ->
- e.preventDefault()
- e.stopImmediatePropagation()
- $this = $(this)
- isClose = $this.hasClass('btn-close')
- shouldSubmit = $this.hasClass('btn-comment')
- if shouldSubmit
- _this.submitNoteForm($this.closest('form'))
- $this.prop('disabled', true)
- url = $this.attr('href')
- $.ajax
- type: 'PUT'
- url: url,
- error: (jqXHR, textStatus, errorThrown) ->
- issueStatus = if isClose then 'close' else 'open'
- new Flash(issueFailMessage, 'alert')
- success: (data, textStatus, jqXHR) ->
- if 'id' of data
- $(document).trigger('issuable:change');
- if isClose
- $('a.btn-close').addClass('hidden')
- $('a.btn-reopen').removeClass('hidden')
- $('div.status-box-closed').removeClass('hidden')
- $('div.status-box-open').addClass('hidden')
- else
- $('a.btn-reopen').addClass('hidden')
- $('a.btn-close').removeClass('hidden')
- $('div.status-box-closed').addClass('hidden')
- $('div.status-box-open').removeClass('hidden')
- else
- new Flash(issueFailMessage, 'alert')
- $this.prop('disabled', false)
-
- submitNoteForm: (form) =>
- noteText = form.find("textarea.js-note-text").val()
- if noteText.trim().length > 0
- form.submit()
-
- disableTaskList: ->
- $('.detail-page-description .js-task-list-container').taskList('disable')
- $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
-
- # TODO (rspeicher): Make the issue description inline-editable like a note so
- # that we can re-use its form here
- updateTaskList: ->
- patchData = {}
- patchData['issue'] = {'description': $('.js-task-list-field', this).val()}
-
- $.ajax
- type: 'PATCH'
- url: $('form.js-issuable-update').attr('action')
- data: patchData
-
- initMergeRequests: ->
- $container = $('#merge-requests')
-
- $.getJSON($container.data('url'))
- .error ->
- new Flash('Failed to load referenced merge requests', 'alert')
- .success (data) ->
- if 'html' of data
- $container.html(data.html)
-
- initRelatedBranches: ->
- $container = $('#related-branches')
-
- $.getJSON($container.data('url'))
- .error ->
- new Flash('Failed to load related branches', 'alert')
- .success (data) ->
- if 'html' of data
- $container.html(data.html)
-
- initCanCreateBranch: ->
- $container = $('div#new-branch')
-
- # If the user doesn't have the required permissions the container isn't
- # rendered at all.
- return if $container.length is 0
-
- $.getJSON($container.data('path'))
- .error ->
- $container.find('.checking').hide()
- $container.find('.unavailable').show()
-
- new Flash('Failed to check if a new branch can be created.', 'alert')
- .success (data) ->
- if data.can_create_branch
- $container.find('.checking').hide()
- $container.find('.available').show()
- $container.find('a').attr('disabled', false)
- else
- $container.find('.checking').hide()
- $container.find('.unavailable').show()
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
new file mode 100644
index 00000000000..076e3972944
--- /dev/null
+++ b/app/assets/javascripts/issue_status_select.js
@@ -0,0 +1,35 @@
+(function() {
+ this.IssueStatusSelect = (function() {
+ function IssueStatusSelect() {
+ $('.js-issue-status').each(function(i, el) {
+ var fieldName;
+ fieldName = $(el).data("field-name");
+ return $(el).glDropdown({
+ selectable: true,
+ fieldName: fieldName,
+ toggleLabel: (function(_this) {
+ return function(selected, el, instance) {
+ var $item, label;
+ label = 'Author';
+ $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ };
+ })(this),
+ clicked: function(item, $el, e) {
+ return e.preventDefault();
+ },
+ id: function(obj, el) {
+ return $(el).data("id");
+ }
+ });
+ });
+ }
+
+ return IssueStatusSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/issue_status_select.js.coffee b/app/assets/javascripts/issue_status_select.js.coffee
deleted file mode 100644
index ed50e2e698f..00000000000
--- a/app/assets/javascripts/issue_status_select.js.coffee
+++ /dev/null
@@ -1,18 +0,0 @@
-class @IssueStatusSelect
- constructor: ->
- $('.js-issue-status').each (i, el) ->
- fieldName = $(el).data("field-name")
-
- $(el).glDropdown(
- selectable: true
- fieldName: fieldName
- toggleLabel: (selected, el, instance) =>
- label = 'Author'
- $item = instance.dropdown.find('.is-active')
- label = $item.text() if $item.length
- label
- clicked: (item, $el, e)->
- e.preventDefault()
- id: (obj, el) ->
- $(el).data("id")
- )
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js
new file mode 100644
index 00000000000..98d3358ba92
--- /dev/null
+++ b/app/assets/javascripts/issues-bulk-assignment.js
@@ -0,0 +1,161 @@
+(function() {
+ this.IssuableBulkActions = (function() {
+ function IssuableBulkActions(opts) {
+ var ref, ref1, ref2;
+ if (opts == null) {
+ opts = {};
+ }
+ this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue');
+ this.form.data('bulkActions', this);
+ this.willUpdateLabels = false;
+ this.bindEvents();
+ Issuable.initChecks();
+ }
+
+ IssuableBulkActions.prototype.getElement = function(selector) {
+ return this.container.find(selector);
+ };
+
+ IssuableBulkActions.prototype.bindEvents = function() {
+ return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
+ };
+
+ IssuableBulkActions.prototype.onFormSubmit = function(e) {
+ e.preventDefault();
+ return this.submit();
+ };
+
+ IssuableBulkActions.prototype.submit = function() {
+ var _this, xhr;
+ _this = this;
+ xhr = $.ajax({
+ url: this.form.attr('action'),
+ method: this.form.attr('method'),
+ dataType: 'JSON',
+ data: this.getFormDataAsObject()
+ });
+ xhr.done(function(response, status, xhr) {
+ return location.reload();
+ });
+ xhr.fail(function() {
+ return new Flash("Issue update failed");
+ });
+ return xhr.always(this.onFormSubmitAlways.bind(this));
+ };
+
+ IssuableBulkActions.prototype.onFormSubmitAlways = function() {
+ return this.form.find('[type="submit"]').enable();
+ };
+
+ IssuableBulkActions.prototype.getSelectedIssues = function() {
+ return this.issues.has('.selected_issue:checked');
+ };
+
+ IssuableBulkActions.prototype.getLabelsFromSelection = function() {
+ var labels;
+ labels = [];
+ this.getSelectedIssues().map(function() {
+ var _labels;
+ _labels = $(this).data('labels');
+ if (_labels) {
+ return _labels.map(function(labelId) {
+ if (labels.indexOf(labelId) === -1) {
+ return labels.push(labelId);
+ }
+ });
+ }
+ });
+ return labels;
+ };
+
+
+ /**
+ * Will return only labels that were marked previously and the user has unmarked
+ * @return {Array} Label IDs
+ */
+
+ IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
+ var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
+ result = [];
+ labelsToKeep = [];
+ ref = this.getElement('.labels-filter .is-indeterminate');
+ for (i = 0, len = ref.length; i < len; i++) {
+ el = ref[i];
+ labelsToKeep.push($(el).data('labelId'));
+ }
+ ref1 = this.getLabelsFromSelection();
+ for (j = 0, len1 = ref1.length; j < len1; j++) {
+ id = ref1[j];
+ if (labelsToKeep.indexOf(id) === -1) {
+ result.push(id);
+ }
+ }
+ return result;
+ };
+
+
+ /**
+ * Simple form serialization, it will return just what we need
+ * Returns key/value pairs from form data
+ */
+
+ IssuableBulkActions.prototype.getFormDataAsObject = function() {
+ var formData;
+ formData = {
+ update: {
+ state_event: this.form.find('input[name="update[state_event]"]').val(),
+ assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
+ milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
+ issues_ids: this.form.find('input[name="update[issues_ids]"]').val(),
+ subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
+ add_label_ids: [],
+ remove_label_ids: []
+ }
+ };
+ 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);
+ });
+ }
+ return formData;
+ };
+
+ IssuableBulkActions.prototype.getLabelsToApply = function() {
+ var $labels, labelIds;
+ labelIds = [];
+ $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
+ $labels.each(function(k, label) {
+ if (label) {
+ return labelIds.push(parseInt($(label).val()));
+ }
+ });
+ return labelIds;
+ };
+
+
+ /**
+ * Returns Label IDs that will be removed from issue selection
+ * @return {Array} Array of labels IDs
+ */
+
+ IssuableBulkActions.prototype.getLabelsToRemove = function() {
+ var indeterminatedLabels, labelsToApply, result;
+ result = [];
+ indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
+ labelsToApply = this.getLabelsToApply();
+ indeterminatedLabels.map(function(id) {
+ if (labelsToApply.indexOf(id) === -1) {
+ return result.push(id);
+ }
+ });
+ return result;
+ };
+
+ return IssuableBulkActions;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/issues-bulk-assignment.js.coffee b/app/assets/javascripts/issues-bulk-assignment.js.coffee
deleted file mode 100644
index 6b0e69dbae7..00000000000
--- a/app/assets/javascripts/issues-bulk-assignment.js.coffee
+++ /dev/null
@@ -1,127 +0,0 @@
-class @IssuableBulkActions
- constructor: (opts = {}) ->
- # Set defaults
- {
- @container = $('.content')
- @form = @getElement('.bulk-update')
- @issues = @getElement('.issues-list .issue')
- } = opts
-
- # Save instance
- @form.data 'bulkActions', @
-
- @willUpdateLabels = false
-
- @bindEvents()
-
- # Fixes bulk-assign not working when navigating through pages
- Issuable.initChecks();
-
- getElement: (selector) ->
- @container.find selector
-
- bindEvents: ->
- @form.off('submit').on('submit', @onFormSubmit.bind(@))
-
- onFormSubmit: (e) ->
- e.preventDefault()
- @submit()
-
- submit: ->
- _this = @
-
- xhr = $.ajax
- url: @form.attr 'action'
- method: @form.attr 'method'
- dataType: 'JSON',
- data: @getFormDataAsObject()
-
- xhr.done (response, status, xhr) ->
- location.reload()
-
- xhr.fail ->
- new Flash("Issue update failed")
-
- xhr.always @onFormSubmitAlways.bind(@)
-
- onFormSubmitAlways: ->
- @form.find('[type="submit"]').enable()
-
- getSelectedIssues: ->
- @issues.has('.selected_issue:checked')
-
- getLabelsFromSelection: ->
- labels = []
-
- @getSelectedIssues().map ->
- _labels = $(@).data('labels')
- if _labels
- _labels.map (labelId) ->
- labels.push(labelId) if labels.indexOf(labelId) is -1
-
- labels
-
- ###*
- * Will return only labels that were marked previously and the user has unmarked
- * @return {Array} Label IDs
- ###
- getUnmarkedIndeterminedLabels: ->
- result = []
- labelsToKeep = []
-
- for el in @getElement('.labels-filter .is-indeterminate')
- labelsToKeep.push $(el).data('labelId')
-
- for id in @getLabelsFromSelection()
- # Only the ones that we are not going to keep
- result.push(id) if labelsToKeep.indexOf(id) is -1
-
- result
-
- ###*
- * Simple form serialization, it will return just what we need
- * Returns key/value pairs from form data
- ###
- getFormDataAsObject: ->
- formData =
- update:
- state_event : @form.find('input[name="update[state_event]"]').val()
- assignee_id : @form.find('input[name="update[assignee_id]"]').val()
- milestone_id : @form.find('input[name="update[milestone_id]"]').val()
- issues_ids : @form.find('input[name="update[issues_ids]"]').val()
- add_label_ids : []
- remove_label_ids : []
-
- if @willUpdateLabels
- @getLabelsToApply().map (id) ->
- formData.update.add_label_ids.push id
-
- @getLabelsToRemove().map (id) ->
- formData.update.remove_label_ids.push id
-
- formData
-
- getLabelsToApply: ->
- labelIds = []
- $labels = @form.find('.labels-filter input[name="update[label_ids][]"]')
-
- $labels.each (k, label) ->
- labelIds.push parseInt($(label).val()) if label
-
- labelIds
-
- ###*
- * Returns Label IDs that will be removed from issue selection
- * @return {Array} Array of labels IDs
- ###
- getLabelsToRemove: ->
- result = []
- indeterminatedLabels = @getUnmarkedIndeterminedLabels()
- labelsToApply = @getLabelsToApply()
-
- indeterminatedLabels.map (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
- result.push(id) if labelsToApply.indexOf(id) is -1
-
- result
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
new file mode 100644
index 00000000000..fe071fca67c
--- /dev/null
+++ b/app/assets/javascripts/labels.js
@@ -0,0 +1,44 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Labels = (function() {
+ function Labels() {
+ this.setSuggestedColor = bind(this.setSuggestedColor, this);
+ this.updateColorPreview = bind(this.updateColorPreview, this);
+ var form;
+ form = $('.label-form');
+ this.cleanBinding();
+ this.addBinding();
+ this.updateColorPreview();
+ }
+
+ Labels.prototype.addBinding = function() {
+ $(document).on('click', '.suggest-colors a', this.setSuggestedColor);
+ return $(document).on('input', 'input#label_color', this.updateColorPreview);
+ };
+
+ Labels.prototype.cleanBinding = function() {
+ $(document).off('click', '.suggest-colors a');
+ return $(document).off('input', 'input#label_color');
+ };
+
+ Labels.prototype.updateColorPreview = function() {
+ var previewColor;
+ previewColor = $('input#label_color').val();
+ return $('div.label-color-preview').css('background-color', previewColor);
+ };
+
+ Labels.prototype.setSuggestedColor = function(e) {
+ var color;
+ color = $(e.currentTarget).data('color');
+ $('input#label_color').val(color);
+ this.updateColorPreview();
+ $('.label-form').trigger('keyup');
+ return e.preventDefault();
+ };
+
+ return Labels;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/labels.js.coffee b/app/assets/javascripts/labels.js.coffee
deleted file mode 100644
index d05bacd7494..00000000000
--- a/app/assets/javascripts/labels.js.coffee
+++ /dev/null
@@ -1,28 +0,0 @@
-class @Labels
- constructor: ->
- form = $('.label-form')
- @cleanBinding()
- @addBinding()
- @updateColorPreview()
-
- addBinding: ->
- $(document).on 'click', '.suggest-colors a', @setSuggestedColor
- $(document).on 'input', 'input#label_color', @updateColorPreview
-
- cleanBinding: ->
- $(document).off 'click', '.suggest-colors a'
- $(document).off 'input', 'input#label_color'
-
- # Updates the the preview color with the hex-color input
- updateColorPreview: =>
- previewColor = $('input#label_color').val()
- $('div.label-color-preview').css('background-color', previewColor)
-
- # Updates the preview color with a click on a suggested color
- setSuggestedColor: (e) =>
- color = $(e.currentTarget).data('color')
- $('input#label_color').val(color)
- @updateColorPreview()
- # Notify the form, that color has changed
- $('.label-form').trigger('keyup')
- e.preventDefault()
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
new file mode 100644
index 00000000000..565dbeacdb3
--- /dev/null
+++ b/app/assets/javascripts/labels_select.js
@@ -0,0 +1,365 @@
+(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, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
+ $dropdown = $(dropdown);
+ projectId = $dropdown.data('project-id');
+ labelUrl = $dropdown.data('labels');
+ issueUpdateURL = $dropdown.data('issueUpdate');
+ selectedLabel = $dropdown.data('selected');
+ if ((selectedLabel != null) && !$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = selectedLabel.split(',');
+ }
+ showNo = $dropdown.data('show-no');
+ showAny = $dropdown.data('show-any');
+ defaultLabel = $dropdown.data('default-label');
+ abilityName = $dropdown.data('ability-name');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ $form = $dropdown.closest('form');
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
+ $value = $block.find('.value');
+ $loading = $block.find('.block-loading').fadeOut();
+ if (issueUpdateURL != null) {
+ issueURLSplit = issueUpdateURL.split('/');
+ }
+ if (issueUpdateURL) {
+ labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
+ labelNoneHTMLTemplate = '<span class="no-value">None</span>';
+ }
+
+ $sidebarLabelTooltip.tooltip();
+
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ }
+
+ saveLabelData = function() {
+ var data, selected;
+ selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
+ return this.value;
+ }).get();
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].label_ids = selected;
+ if (!selected.length) {
+ data[abilityName].label_ids = [''];
+ }
+ $loading.fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ dataType: 'JSON',
+ data: data
+ }).done(function(data) {
+ var labelCount, template, labelTooltipTitle, labelTitles;
+ $loading.fadeOut();
+ $dropdown.trigger('loaded.gl.dropdown');
+ $selectbox.hide();
+ data.issueURLSplit = issueURLSplit;
+ labelCount = 0;
+ if (data.labels.length) {
+ template = labelHTMLTemplate(data);
+ labelCount = data.labels.length;
+ } else {
+ template = labelNoneHTMLTemplate;
+ }
+ $value.removeAttr('style').html(template);
+ $sidebarCollapsedValue.text(labelCount);
+
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
+
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+ }
+
+ labelTooltipTitle = labelTitles.join(', ');
+ } else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
+
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
+
+ $('.has-tooltip', $value).tooltip({
+ container: 'body'
+ });
+ return $value.find('a').each(function(i) {
+ return setTimeout((function(_this) {
+ return function() {
+ return gl.animate.animate($(_this), 'pulse');
+ };
+ })(this), 200 * i);
+ });
+ });
+ };
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: labelUrl
+ }).done(function(data) {
+ data = _.chain(data).groupBy(function(label) {
+ return label.title;
+ }).map(function(label) {
+ var color;
+ color = _.map(label, function(dup) {
+ return dup.color;
+ });
+ return {
+ id: label[0].id,
+ title: label[0].title,
+ color: color,
+ duplicate: color.length > 1
+ };
+ }).value();
+ if ($dropdown.hasClass('js-extra-options')) {
+ if (showNo) {
+ data.unshift({
+ id: 0,
+ title: 'No Label'
+ });
+ }
+ if (showAny) {
+ data.unshift({
+ isAny: true,
+ title: 'Any Label'
+ });
+ }
+ if (data.length > 2) {
+ data.splice(2, 0, 'divider');
+ }
+ }
+ return callback(data);
+ });
+ },
+ renderRow: function(label, instance) {
+ var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+ $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;
+ if (indeterminate.indexOf(label.id) !== -1) {
+ selectedClass.push('is-indeterminate');
+ }
+ if (active.indexOf(label.id) !== -1) {
+ i = selectedClass.indexOf('is-indeterminate');
+ if (i !== -1) {
+ selectedClass.splice(i, 1);
+ }
+ selectedClass.push('is-active');
+ instance.addInput(this.fieldName, label.id);
+ }
+ }
+ if ($form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + (this.id(label)) + "']").length) {
+ selectedClass.push('is-active');
+ }
+ if ($dropdown.hasClass('js-multiselect') && removesAll) {
+ selectedClass.push('dropdown-clear-active');
+ }
+ if (label.duplicate) {
+ spacing = 100 / label.color.length;
+ label.color = label.color.filter(function(color, i) {
+ return i < 4;
+ });
+ color = _.map(label.color, function(color, i) {
+ var percentFirst, percentSecond;
+ percentFirst = Math.floor(spacing * i);
+ percentSecond = Math.floor(spacing * (i + 1));
+ return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
+ }).join(',');
+ color = "linear-gradient(" + color + ")";
+ } else {
+ if (label.color != null) {
+ color = label.color[0];
+ }
+ }
+ if (color) {
+ colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
+ } else {
+ colorEl = '';
+ }
+ if (label.id) {
+ selectedClass.push('label-item');
+ $a.attr('data-label-id', label.id);
+ }
+ $a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ return $li.html($a).prop('outerHTML');
+ },
+ persistWhenHide: $dropdown.data('persistWhenHide'),
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ filterable: true,
+ toggleLabel: function(selected, el) {
+ var selected_labels;
+ selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
+ if (selected && (selected.title != null)) {
+ if (selected_labels.length > 1) {
+ return selected.title + " +" + (selected_labels.length - 1) + " more";
+ } else {
+ return selected.title;
+ }
+ } else if (!selected && selected_labels.length !== 0) {
+ if (selected_labels.length > 1) {
+ return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
+ } else if (selected_labels.length === 1) {
+ return $(selected_labels).text();
+ }
+ } else {
+ return defaultLabel;
+ }
+ },
+ fieldName: $dropdown.data('field-name'),
+ id: function(label) {
+ if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
+ return label.title;
+ } else {
+ return label.id;
+ }
+ },
+ hidden: function() {
+ var isIssueIndex, isMRIndex, page, selectedLabels;
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
+ $selectbox.hide();
+ $value.removeAttr('style');
+ if (page === 'projects:boards:show') {
+ return;
+ }
+ if ($dropdown.hasClass('js-multiselect')) {
+ if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
+ Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ $dropdown.closest('form').submit();
+ } else {
+ if (!$dropdown.hasClass('js-filter-bulk-update')) {
+ saveLabelData();
+ }
+ }
+ }
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ if (!this.options.persistWhenHide) {
+ return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
+ }
+ }
+ },
+ multiSelect: $dropdown.hasClass('js-multiselect'),
+ clicked: function(label, $el, e) {
+ var isIssueIndex, isMRIndex, page;
+ _this.enableBulkLabelDropdown();
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ return;
+ }
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = page === 'projects:merge_requests:index';
+ if (page === 'projects:boards:show') {
+ if (label.isAny) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
+ } else if (label.title) {
+ gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
+ } else {
+ var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
+ filters = filters.filter(function (label) {
+ return label !== $el.text().trim();
+ });
+ gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
+ }
+
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ return;
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (!$dropdown.hasClass('js-multiselect')) {
+ selectedLabel = label.title;
+ return Issuable.filterResults($dropdown.closest('form'));
+ }
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else {
+ if ($dropdown.hasClass('js-multiselect')) {
+
+ } else {
+ return saveLabelData();
+ }
+ }
+ },
+ 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();
+ }
+ }
+ });
+ });
+ this.bindEvents();
+ }
+
+ LabelsSelect.prototype.bindEvents = function() {
+ return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
+ };
+
+ LabelsSelect.prototype.onSelectCheckboxIssue = function() {
+ if ($('.selected_issue:checked').length) {
+ return;
+ }
+ $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
+ 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) {
+ issuableBulkActions = $('.bulk-update').data('bulkActions');
+ return issuableBulkActions.willUpdateLabels = true;
+ }
+ };
+
+ return LabelsSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
deleted file mode 100644
index 7688609b301..00000000000
--- a/app/assets/javascripts/labels_select.js.coffee
+++ /dev/null
@@ -1,386 +0,0 @@
-class @LabelsSelect
- constructor: ->
- _this = @
-
- $('.js-label-select').each (i, dropdown) ->
- $dropdown = $(dropdown)
- projectId = $dropdown.data('project-id')
- labelUrl = $dropdown.data('labels')
- issueUpdateURL = $dropdown.data('issueUpdate')
- selectedLabel = $dropdown.data('selected')
- if selectedLabel? and not $dropdown.hasClass 'js-multiselect'
- selectedLabel = selectedLabel.split(',')
- newLabelField = $('#new_label_name')
- newColorField = $('#new_label_color')
- showNo = $dropdown.data('show-no')
- showAny = $dropdown.data('show-any')
- defaultLabel = $dropdown.data('default-label')
- abilityName = $dropdown.data('ability-name')
- $selectbox = $dropdown.closest('.selectbox')
- $block = $selectbox.closest('.block')
- $form = $dropdown.closest('form')
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
- $value = $block.find('.value')
- $newLabelError = $('.js-label-error')
- $colorPreview = $('.js-dropdown-label-color-preview')
- $newLabelCreateButton = $('.js-new-label-btn')
-
- $newLabelError.hide()
- $loading = $block.find('.block-loading').fadeOut()
-
- issueURLSplit = issueUpdateURL.split('/') if issueUpdateURL?
- if issueUpdateURL
- labelHTMLTemplate = _.template(
- '<% _.each(labels, function(label){ %>
- <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>">
- <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">
- <%- label.title %>
- </span>
- </a>
- <% }); %>'
- )
- labelNoneHTMLTemplate = '<span class="no-value">None</span>'
-
- if newLabelField.length
-
- # Suggested colors in the dropdown to chose from pre-chosen colors
- $('.suggest-colors-dropdown a').on "click", (e) ->
- e.preventDefault()
- e.stopPropagation()
- newColorField
- .val($(this).data('color'))
- .trigger('change')
- $colorPreview
- .css 'background-color', $(this).data('color')
- .parent()
- .addClass 'is-active'
-
- # Cancel button takes back to first page
- resetForm = ->
- newLabelField
- .val ''
- .trigger 'change'
- newColorField
- .val ''
- .trigger 'change'
- $colorPreview
- .css 'background-color', ''
- .parent()
- .removeClass 'is-active'
-
- $('.dropdown-menu-back').on 'click', ->
- resetForm()
-
- $('.js-cancel-label-btn').on 'click', (e) ->
- e.preventDefault()
- e.stopPropagation()
- resetForm()
- $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
-
- # Listen for change and keyup events on label and color field
- # This allows us to enable the button when ready
- enableLabelCreateButton = ->
- if newLabelField.val() isnt '' and newColorField.val() isnt ''
- $newLabelError.hide()
- $newLabelCreateButton.enable()
- else
- $newLabelCreateButton.disable()
-
- saveLabel = ->
- # Create new label with API
- Api.newLabel projectId, {
- name: newLabelField.val()
- color: newColorField.val()
- }, (label) ->
- $newLabelCreateButton.enable()
-
- if label.message?
- errors = _.map label.message, (value, key) ->
- "#{key} #{value[0]}"
-
- $newLabelError
- .html errors.join("<br/>")
- .show()
- else
- $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
-
- newLabelField.on 'keyup change', enableLabelCreateButton
-
- newColorField.on 'keyup change', enableLabelCreateButton
-
- # Send the API call to create the label
- $newLabelCreateButton
- .disable()
- .on 'click', (e) ->
- e.preventDefault()
- e.stopPropagation()
- saveLabel()
-
- saveLabelData = ->
- selected = $dropdown
- .closest('.selectbox')
- .find("input[name='#{$dropdown.data('field-name')}']")
- .map(->
- @value
- ).get()
- data = {}
- data[abilityName] = {}
- data[abilityName].label_ids = selected
- if not selected.length
- data[abilityName].label_ids = ['']
- $loading.fadeIn()
- $dropdown.trigger('loading.gl.dropdown')
- $.ajax(
- type: 'PUT'
- url: issueUpdateURL
- dataType: 'JSON'
- data: data
- ).done (data) ->
- $loading.fadeOut()
- $dropdown.trigger('loaded.gl.dropdown')
- $selectbox.hide()
- data.issueURLSplit = issueURLSplit
- labelCount = 0
- if data.labels.length
- template = labelHTMLTemplate(data)
- labelCount = data.labels.length
- else
- template = labelNoneHTMLTemplate
- $value
- .removeAttr('style')
- .html(template)
- $sidebarCollapsedValue.text(labelCount)
-
- $('.has-tooltip', $value).tooltip(container: 'body')
-
- $value
- .find('a')
- .each((i) ->
- setTimeout(=>
- gl.animate.animate($(@), 'pulse')
- ,200 * i
- )
- )
-
-
- $dropdown.glDropdown(
- data: (term, callback) ->
- $.ajax(
- url: labelUrl
- ).done (data) ->
- data = _.chain data
- .groupBy (label) ->
- label.title
- .map (label) ->
- color = _.map label, (dup) ->
- dup.color
-
- return {
- id: label[0].id
- title: label[0].title
- color: color
- duplicate: color.length > 1
- }
- .value()
-
- if $dropdown.hasClass 'js-extra-options'
- if showNo
- data.unshift(
- id: 0
- title: 'No Label'
- )
-
- if showAny
- data.unshift(
- isAny: true
- title: 'Any Label'
- )
-
- if data.length > 2
- data.splice 2, 0, 'divider'
-
- callback data
-
- renderRow: (label, instance) ->
- $li = $('<li>')
- $a = $('<a href="#">')
-
- selectedClass = []
- removesAll = label.id is 0 or not label.id?
-
- if $dropdown.hasClass('js-filter-bulk-update')
- indeterminate = instance.indeterminateIds
- active = instance.activeIds
-
- if indeterminate.indexOf(label.id) isnt -1
- selectedClass.push 'is-indeterminate'
-
- if active.indexOf(label.id) isnt -1
- # Remove is-indeterminate class if the item will be marked as active
- i = selectedClass.indexOf 'is-indeterminate'
- selectedClass.splice i, 1 unless i is -1
-
- selectedClass.push 'is-active'
-
- # Add input manually
- instance.addInput @fieldName, label.id
-
- if $form.find("input[type='hidden']\
- [name='#{$dropdown.data('fieldName')}']\
- [value='#{this.id(label)}']").length
- selectedClass.push 'is-active'
-
- if $dropdown.hasClass('js-multiselect') and removesAll
- selectedClass.push 'dropdown-clear-active'
-
- if label.duplicate
- spacing = 100 / label.color.length
-
- # Reduce the colors to 4
- label.color = label.color.filter (color, i) ->
- i < 4
-
- color = _.map(label.color, (color, i) ->
- percentFirst = Math.floor(spacing * i)
- percentSecond = Math.floor(spacing * (i + 1))
- "#{color} #{percentFirst}%,#{color} #{percentSecond}% "
- ).join(',')
- color = "linear-gradient(#{color})"
- else
- if label.color?
- color = label.color[0]
-
- if color
- colorEl = "<span class='dropdown-label-box' style='background: #{color}'></span>"
- else
- colorEl = ''
-
- # We need to identify which items are actually labels
- if label.id
- selectedClass.push('label-item')
- $a.attr('data-label-id', label.id)
-
- $a.addClass(selectedClass.join(' '))
- .html("#{colorEl} #{label.title}")
-
- # Return generated html
- $li.html($a).prop('outerHTML')
- persistWhenHide: $dropdown.data('persistWhenHide')
- search:
- fields: ['title']
- selectable: true
- filterable: true
- toggleLabel: (selected, el) ->
- selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
-
- if selected and selected.title?
- if selected_labels.length > 1
- "#{selected.title} +#{selected_labels.length - 1} more"
- else
- selected.title
- else if not selected and selected_labels.length isnt 0
- if selected_labels.length > 1
- "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more"
- else if selected_labels.length is 1
- $(selected_labels).text()
- else
- defaultLabel
- fieldName: $dropdown.data('field-name')
- id: (label) ->
- if $dropdown.hasClass("js-filter-submit") and not label.isAny?
- label.title
- else
- label.id
-
- hidden: ->
- page = $('body').data 'page'
- isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is 'projects:merge_requests:index'
-
- $selectbox.hide()
- # display:block overrides the hide-collapse rule
- $value.removeAttr('style')
- if $dropdown.hasClass 'js-multiselect'
- if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- selectedLabels = $dropdown
- .closest('form')
- .find("input:hidden[name='#{$dropdown.data('fieldName')}']")
- Issuable.filterResults $dropdown.closest('form')
- else if $dropdown.hasClass('js-filter-submit')
- $dropdown.closest('form').submit()
- else
- if not $dropdown.hasClass 'js-filter-bulk-update'
- saveLabelData()
-
- if $dropdown.hasClass('js-filter-bulk-update')
- # If we are persisting state we need the classes
- if not @options.persistWhenHide
- $dropdown.parent().find('.is-active, .is-indeterminate').removeClass()
-
- multiSelect: $dropdown.hasClass 'js-multiselect'
- clicked: (label) ->
- _this.enableBulkLabelDropdown()
-
- if $dropdown.hasClass('js-filter-bulk-update')
- return
-
- page = $('body').data 'page'
- isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is 'projects:merge_requests:index'
- if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- if not $dropdown.hasClass 'js-multiselect'
- selectedLabel = label.title
- Issuable.filterResults $dropdown.closest('form')
- else if $dropdown.hasClass 'js-filter-submit'
- $dropdown.closest('form').submit()
- else
- if $dropdown.hasClass 'js-multiselect'
- return
- else
- saveLabelData()
-
- setIndeterminateIds: ->
- if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
- @indeterminateIds = _this.getIndeterminateIds()
-
- setActiveIds: ->
- if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
- @activeIds = _this.getActiveIds()
- )
-
- @bindEvents()
-
- bindEvents: ->
- $('body').on 'change', '.selected_issue', @onSelectCheckboxIssue
-
- onSelectCheckboxIssue: ->
- return if $('.selected_issue:checked').length
-
- # Remove inputs
- $('.issues_bulk_update .labels-filter input[type="hidden"]').remove()
-
- # Also restore button text
- $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label')
-
- getIndeterminateIds: ->
- label_ids = []
-
- $('.selected_issue:checked').each (i, el) ->
- issue_id = $(el).data('id')
- label_ids.push $("#issue_#{issue_id}").data('labels')
-
- _.flatten(label_ids)
-
- getActiveIds: ->
- label_ids = []
-
- $('.selected_issue:checked').each (i, el) ->
- issue_id = $(el).data('id')
- label_ids.push $("#issue_#{issue_id}").data('labels')
-
- _.intersection.apply _, label_ids
-
- enableBulkLabelDropdown: ->
- if $('.selected_issue:checked').length
- issuableBulkActions = $('.bulk-update').data('bulkActions')
- issuableBulkActions.willUpdateLabels = true
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
new file mode 100644
index 00000000000..ce472f3bcd0
--- /dev/null
+++ b/app/assets/javascripts/layout_nav.js
@@ -0,0 +1,27 @@
+(function() {
+ var hideEndFade;
+
+ hideEndFade = function($scrollingTabs) {
+ return $scrollingTabs.each(function() {
+ var $this;
+ $this = $(this);
+ return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
+ });
+ };
+
+ $(function() {
+ hideEndFade($('.scrolling-tabs'));
+ $(window).off('resize.nav').on('resize.nav', function() {
+ return hideEndFade($('.scrolling-tabs'));
+ });
+ return $('.scrolling-tabs').on('scroll', function(event) {
+ var $this, currentPosition, maxPosition;
+ $this = $(this);
+ currentPosition = $this.scrollLeft();
+ maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+ $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
+ return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/layout_nav.js.coffee b/app/assets/javascripts/layout_nav.js.coffee
deleted file mode 100644
index f639f7f5892..00000000000
--- a/app/assets/javascripts/layout_nav.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-hideEndFade = ($scrollingTabs) ->
- $scrollingTabs.each ->
- $this = $(@)
-
- $this
- .siblings('.fade-right')
- .toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'))
-
-$ ->
-
- hideEndFade($('.scrolling-tabs'))
-
- $(window)
- .off 'resize.nav'
- .on 'resize.nav', ->
- hideEndFade($('.scrolling-tabs'))
-
- $('.scrolling-tabs').on 'scroll', (event) ->
- $this = $(this)
- currentPosition = $this.scrollLeft()
- maxPosition = $this.prop('scrollWidth') - $this.outerWidth()
-
- $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0)
- $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1)
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 00000000000..4cdf99cae72
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,2 @@
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
new file mode 100644
index 00000000000..8d5e52286b7
--- /dev/null
+++ b/app/assets/javascripts/lib/chart.js
@@ -0,0 +1,7 @@
+
+/*= require Chart */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/chart.js.coffee b/app/assets/javascripts/lib/chart.js.coffee
deleted file mode 100644
index 82217fc5107..00000000000
--- a/app/assets/javascripts/lib/chart.js.coffee
+++ /dev/null
@@ -1 +0,0 @@
-#= require Chart
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
new file mode 100644
index 00000000000..8ee81804513
--- /dev/null
+++ b/app/assets/javascripts/lib/cropper.js
@@ -0,0 +1,7 @@
+
+/*= require cropper */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/cropper.js.coffee b/app/assets/javascripts/lib/cropper.js.coffee
deleted file mode 100644
index 32536d23fe3..00000000000
--- a/app/assets/javascripts/lib/cropper.js.coffee
+++ /dev/null
@@ -1 +0,0 @@
-#= require cropper
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
new file mode 100644
index 00000000000..31e6033e756
--- /dev/null
+++ b/app/assets/javascripts/lib/d3.js
@@ -0,0 +1,7 @@
+
+/*= require d3 */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/d3.js.coffee b/app/assets/javascripts/lib/d3.js.coffee
deleted file mode 100644
index 74f0a0bb06a..00000000000
--- a/app/assets/javascripts/lib/d3.js.coffee
+++ /dev/null
@@ -1 +0,0 @@
-#= require d3
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
new file mode 100644
index 00000000000..923c575dcfe
--- /dev/null
+++ b/app/assets/javascripts/lib/raphael.js
@@ -0,0 +1,13 @@
+
+/*= require raphael */
+
+
+/*= require g.raphael */
+
+
+/*= require g.bar */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/raphael.js.coffee b/app/assets/javascripts/lib/raphael.js.coffee
deleted file mode 100644
index ab8e5979b87..00000000000
--- a/app/assets/javascripts/lib/raphael.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-#= require raphael
-#= require g.raphael
-#= require g.bar
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
new file mode 100644
index 00000000000..d36efdabc93
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -0,0 +1,49 @@
+(function() {
+ (function(w) {
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if (gl.animate == null) {
+ gl.animate = {};
+ }
+ gl.animate.animate = function($el, animation, options, done) {
+ if ((options != null ? options.cssStart : void 0) != null) {
+ $el.css(options.cssStart);
+ }
+ $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() {
+ $(this).removeClass(animation + ' animated');
+ if (done != null) {
+ done();
+ }
+ if ((options != null ? options.cssEnd : void 0) != null) {
+ $el.css(options.cssEnd);
+ }
+ });
+ };
+ gl.animate.animateEach = function($els, animation, time, options, done) {
+ var dfd;
+ dfd = $.Deferred();
+ if (!$els.length) {
+ dfd.resolve();
+ }
+ $els.each(function(i) {
+ setTimeout((function(_this) {
+ return function() {
+ var $this;
+ $this = $(_this);
+ return gl.animate.animate($this, animation, options, function() {
+ if (i === $els.length - 1) {
+ dfd.resolve();
+ if (done != null) {
+ return done();
+ }
+ }
+ });
+ };
+ })(this), time * i);
+ });
+ return dfd.promise();
+ };
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/animate.js.coffee b/app/assets/javascripts/lib/utils/animate.js.coffee
deleted file mode 100644
index ec3b44d6126..00000000000
--- a/app/assets/javascripts/lib/utils/animate.js.coffee
+++ /dev/null
@@ -1,39 +0,0 @@
-((w) ->
- if not w.gl? then w.gl = {}
- if not gl.animate? then gl.animate = {}
-
- gl.animate.animate = ($el, animation, options, done) ->
- if options?.cssStart?
- $el.css(options.cssStart)
- $el
- .removeClass(animation + ' animated')
- .addClass(animation + ' animated')
- .one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
- $(this).removeClass(animation + ' animated')
- if done?
- done()
- if options?.cssEnd?
- $el.css(options.cssEnd)
- return
- return
-
- gl.animate.animateEach = ($els, animation, time, options, done) ->
- dfd = $.Deferred()
- if not $els.length
- dfd.resolve()
- $els.each((i) ->
- setTimeout(=>
- $this = $(@)
- gl.animate.animate($this, animation, options, =>
- if i is $els.length - 1
- dfd.resolve()
- if done?
- done()
- )
- ,time * i
- )
- return
- )
- return dfd.promise()
- return
-) window \ No newline at end of file
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
new file mode 100644
index 00000000000..9299d0eabd2
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -0,0 +1,60 @@
+(function() {
+ (function(w) {
+ var base;
+ w.gl || (w.gl = {});
+ (base = w.gl).utils || (base.utils = {});
+ w.gl.utils.isInGroupsPage = function() {
+ return gl.utils.getPagePath() === 'groups';
+ };
+ w.gl.utils.isInProjectPage = function() {
+ return gl.utils.getPagePath() === 'projects';
+ };
+ w.gl.utils.getProjectSlug = function() {
+ if (this.isInProjectPage()) {
+ return $('body').data('project');
+ } else {
+ return null;
+ }
+ };
+ w.gl.utils.getGroupSlug = function() {
+ if (this.isInGroupsPage()) {
+ return $('body').data('group');
+ } else {
+ return null;
+ }
+ };
+ 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;
+ }
+ });
+ };
+ gl.utils.getPagePath = function() {
+ return $('body').data('page').split(':')[0];
+ };
+ 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;
+ };
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.coffee b/app/assets/javascripts/lib/utils/common_utils.js.coffee
deleted file mode 100644
index d4dd3dc329a..00000000000
--- a/app/assets/javascripts/lib/utils/common_utils.js.coffee
+++ /dev/null
@@ -1,68 +0,0 @@
-((w) ->
-
- w.gl or= {}
- w.gl.utils or= {}
-
- w.gl.utils.isInGroupsPage = ->
-
- return gl.utils.getPagePath() is 'groups'
-
-
- w.gl.utils.isInProjectPage = ->
-
- return gl.utils.getPagePath() is 'projects'
-
-
- w.gl.utils.getProjectSlug = ->
-
- return if @isInProjectPage() then $('body').data 'project' else null
-
-
- w.gl.utils.getGroupSlug = ->
-
- return if @isInGroupsPage() then $('body').data 'group' else null
-
-
-
- gl.utils.updateTooltipTitle = ($tooltipEl, newTitle) ->
-
- $tooltipEl
- .tooltip 'destroy'
- .attr 'title', newTitle
- .tooltip 'fixTitle'
-
-
- gl.utils.preventDisabledButtons = ->
-
- $('.btn').click (e) ->
- if $(this).hasClass 'disabled'
- e.preventDefault()
- e.stopImmediatePropagation()
- return false
-
- gl.utils.getPagePath = ->
- return $('body').data('page').split(':')[0]
-
-
- jQuery.timefor = (time, suffix, expiredLabel) ->
-
- return '' unless time
-
- suffix or= 'remaining'
- expiredLabel or= 'Past due'
-
- jQuery.timeago.settings.allowFuture = yes
-
- { suffixFromNow } = jQuery.timeago.settings.strings
- jQuery.timeago.settings.strings.suffixFromNow = suffix
-
- timefor = $.timeago time
-
- if timefor.indexOf('ago') > -1
- timefor = expiredLabel
-
- jQuery.timeago.settings.strings.suffixFromNow = suffixFromNow
-
- return timefor
-
-) window
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
new file mode 100644
index 00000000000..d4d5927d3b0
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -0,0 +1,80 @@
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+
+ w.gl.utils.formatDate = function(datetime) {
+ return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+ };
+
+ w.gl.utils.getDayName = function(date) {
+ return this.days[date.getDay()];
+ };
+
+ w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
+ if (setTimeago == null) {
+ setTimeago = true;
+ }
+ $timeagoEls.each(function() {
+ var $el;
+ $el = $(this);
+ return $el.attr('title', gl.utils.formatDate($el.attr('datetime')));
+ });
+ if (setTimeago) {
+ $timeagoEls.timeago();
+ $timeagoEls.tooltip('destroy');
+ 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: []
+ };
+ 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;
+ };
+
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ }
+
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee b/app/assets/javascripts/lib/utils/datetime_utility.js.coffee
deleted file mode 100644
index 948d6dbf07e..00000000000
--- a/app/assets/javascripts/lib/utils/datetime_utility.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-((w) ->
-
- w.gl ?= {}
- w.gl.utils ?= {}
-
- w.gl.utils.formatDate = (datetime) ->
- dateFormat(datetime, 'mmm d, yyyy h:MMtt Z')
-
- w.gl.utils.localTimeAgo = ($timeagoEls, setTimeago = true) ->
- $timeagoEls.each( ->
- $el = $(@)
- $el.attr('title', gl.utils.formatDate($el.attr('datetime')))
- )
-
- if setTimeago
- $timeagoEls.timeago()
- $timeagoEls.tooltip('destroy')
-
- # Recreate with custom template
- $timeagoEls.tooltip(
- template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
- )
-
-) window
diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js
deleted file mode 100644
index b63716eaad2..00000000000
--- a/app/assets/javascripts/lib/utils/md5.js
+++ /dev/null
@@ -1,211 +0,0 @@
-function md5 (str) {
- // http://kevin.vanzonneveld.net
- // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
- // + namespaced by: Michael White (http://getsprink.com)
- // + tweaked by: Jack
- // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
- // + input by: Brett Zamir (http://brett-zamir.me)
- // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
- // - depends on: utf8_encode
- // * example 1: md5('Kevin van Zonneveld');
- // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9'
- var xl;
-
- var rotateLeft = function (lValue, iShiftBits) {
- return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits));
- };
-
- var addUnsigned = function (lX, lY) {
- var lX4, lY4, lX8, lY8, lResult;
- lX8 = (lX & 0x80000000);
- lY8 = (lY & 0x80000000);
- lX4 = (lX & 0x40000000);
- lY4 = (lY & 0x40000000);
- lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF);
- if (lX4 & lY4) {
- return (lResult ^ 0x80000000 ^ lX8 ^ lY8);
- }
- if (lX4 | lY4) {
- if (lResult & 0x40000000) {
- return (lResult ^ 0xC0000000 ^ lX8 ^ lY8);
- } else {
- return (lResult ^ 0x40000000 ^ lX8 ^ lY8);
- }
- } else {
- return (lResult ^ lX8 ^ lY8);
- }
- };
-
- var _F = function (x, y, z) {
- return (x & y) | ((~x) & z);
- };
- var _G = function (x, y, z) {
- return (x & z) | (y & (~z));
- };
- var _H = function (x, y, z) {
- return (x ^ y ^ z);
- };
- var _I = function (x, y, z) {
- return (y ^ (x | (~z)));
- };
-
- var _FF = function (a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac));
- return addUnsigned(rotateLeft(a, s), b);
- };
-
- var _GG = function (a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac));
- return addUnsigned(rotateLeft(a, s), b);
- };
-
- var _HH = function (a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac));
- return addUnsigned(rotateLeft(a, s), b);
- };
-
- var _II = function (a, b, c, d, x, s, ac) {
- a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac));
- return addUnsigned(rotateLeft(a, s), b);
- };
-
- var convertToWordArray = function (str) {
- var lWordCount;
- var lMessageLength = str.length;
- var lNumberOfWords_temp1 = lMessageLength + 8;
- var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64;
- var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16;
- var lWordArray = new Array(lNumberOfWords - 1);
- var lBytePosition = 0;
- var lByteCount = 0;
- while (lByteCount < lMessageLength) {
- lWordCount = (lByteCount - (lByteCount % 4)) / 4;
- lBytePosition = (lByteCount % 4) * 8;
- lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition));
- lByteCount++;
- }
- lWordCount = (lByteCount - (lByteCount % 4)) / 4;
- lBytePosition = (lByteCount % 4) * 8;
- lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition);
- lWordArray[lNumberOfWords - 2] = lMessageLength << 3;
- lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29;
- return lWordArray;
- };
-
- var wordToHex = function (lValue) {
- var wordToHexValue = "",
- wordToHexValue_temp = "",
- lByte, lCount;
- for (lCount = 0; lCount <= 3; lCount++) {
- lByte = (lValue >>> (lCount * 8)) & 255;
- wordToHexValue_temp = "0" + lByte.toString(16);
- wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2);
- }
- return wordToHexValue;
- };
-
- var x = [],
- k, AA, BB, CC, DD, a, b, c, d, S11 = 7,
- S12 = 12,
- S13 = 17,
- S14 = 22,
- S21 = 5,
- S22 = 9,
- S23 = 14,
- S24 = 20,
- S31 = 4,
- S32 = 11,
- S33 = 16,
- S34 = 23,
- S41 = 6,
- S42 = 10,
- S43 = 15,
- S44 = 21;
-
- str = this.utf8_encode(str);
- x = convertToWordArray(str);
- a = 0x67452301;
- b = 0xEFCDAB89;
- c = 0x98BADCFE;
- d = 0x10325476;
-
- xl = x.length;
- for (k = 0; k < xl; k += 16) {
- AA = a;
- BB = b;
- CC = c;
- DD = d;
- a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478);
- d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756);
- c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB);
- b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE);
- a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF);
- d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A);
- c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613);
- b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501);
- a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8);
- d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF);
- c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1);
- b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE);
- a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122);
- d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193);
- c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E);
- b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821);
- a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562);
- d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340);
- c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51);
- b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA);
- a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D);
- d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453);
- c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681);
- b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8);
- a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6);
- d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6);
- c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87);
- b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED);
- a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905);
- d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8);
- c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9);
- b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A);
- a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942);
- d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681);
- c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122);
- b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C);
- a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44);
- d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9);
- c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60);
- b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70);
- a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6);
- d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA);
- c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085);
- b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05);
- a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039);
- d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5);
- c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8);
- b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665);
- a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244);
- d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97);
- c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7);
- b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039);
- a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3);
- d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92);
- c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D);
- b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1);
- a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F);
- d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0);
- c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314);
- b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1);
- a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82);
- d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235);
- c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB);
- b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391);
- a = addUnsigned(a, AA);
- b = addUnsigned(b, BB);
- c = addUnsigned(c, CC);
- d = addUnsigned(d, DD);
- }
-
- var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d);
-
- return temp.toLowerCase();
-}
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
new file mode 100644
index 00000000000..42b6ac0589e
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -0,0 +1,41 @@
+(function() {
+ (function(w) {
+ var notificationGranted, notifyMe, notifyPermissions;
+ notificationGranted = function(message, opts, onclick) {
+ var notification;
+ notification = new Notification(message, opts);
+ setTimeout(function() {
+ return notification.close();
+ }, 8000);
+ if (onclick) {
+ return notification.onclick = onclick;
+ }
+ };
+ notifyPermissions = function() {
+ if ('Notification' in window) {
+ return Notification.requestPermission();
+ }
+ };
+ notifyMe = function(message, body, icon, onclick) {
+ var opts;
+ opts = {
+ body: body,
+ icon: icon
+ };
+ if (!('Notification' in window)) {
+
+ } else if (Notification.permission === 'granted') {
+ return notificationGranted(message, opts, onclick);
+ } else if (Notification.permission !== 'denied') {
+ return Notification.requestPermission(function(permission) {
+ if (permission === 'granted') {
+ return notificationGranted(message, opts, onclick);
+ }
+ });
+ }
+ };
+ w.notify = notifyMe;
+ return w.notifyPermissions = notifyPermissions;
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js.coffee b/app/assets/javascripts/lib/utils/notify.js.coffee
deleted file mode 100644
index 9e28353ac34..00000000000
--- a/app/assets/javascripts/lib/utils/notify.js.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-((w) ->
- notificationGranted = (message, opts, onclick) ->
- notification = new Notification(message, opts)
-
- # Hide the notification after X amount of seconds
- setTimeout ->
- notification.close()
- , 8000
-
- if onclick
- notification.onclick = onclick
-
- notifyPermissions = ->
- if 'Notification' of window
- Notification.requestPermission()
-
- notifyMe = (message, body, icon, onclick) ->
- opts =
- body: body
- icon: icon
- # Let's check if the browser supports notifications
- if !('Notification' of window)
- # do nothing
- else if Notification.permission == 'granted'
- # If it's okay let's create a notification
- notificationGranted message, opts, onclick
- else if Notification.permission != 'denied'
- Notification.requestPermission (permission) ->
- # If the user accepts, let's create a notification
- if permission == 'granted'
- notificationGranted message, opts, onclick
-
- w.notify = notifyMe
- w.notifyPermissions = notifyPermissions
-) window
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
new file mode 100644
index 00000000000..b6636de5767
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -0,0 +1,115 @@
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).text == null) {
+ base.text = {};
+ }
+ gl.text.randomString = function() {
+ return Math.random().toString(36).substring(7);
+ };
+ gl.text.replaceRange = function(s, start, end, substitute) {
+ return s.substring(0, start) + substitute + s.substring(end);
+ };
+ gl.text.selectedText = function(text, textarea) {
+ return text.substring(textarea.selectionStart, textarea.selectionEnd);
+ };
+ gl.text.lineBefore = function(text, textarea) {
+ var split;
+ split = text.substring(0, textarea.selectionStart).trim().split('\n');
+ return split[split.length - 1];
+ };
+ gl.text.lineAfter = function(text, textarea) {
+ return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+ };
+ gl.text.blockTagText = function(text, textArea, blockTag, selected) {
+ var lineAfter, lineBefore;
+ lineBefore = this.lineBefore(text, textArea);
+ lineAfter = this.lineAfter(text, textArea);
+ if (lineBefore === blockTag && lineAfter === blockTag) {
+ if (blockTag != null) {
+ textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+ textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
+ }
+ return selected;
+ } else {
+ return blockTag + "\n" + selected + "\n" + blockTag;
+ }
+ };
+ gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+ var insertText, inserted, selectedSplit, startChar;
+ selectedSplit = selected.split('\n');
+ startChar = !wrap && textArea.selectionStart > 0 ? '\n' : '';
+ if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
+ if (blockTag != null) {
+ insertText = this.blockTagText(text, textArea, blockTag, selected);
+ } else {
+ insertText = selectedSplit.map(function(val) {
+ if (val.indexOf(tag) === 0) {
+ return "" + (val.replace(tag, ''));
+ } else {
+ return "" + tag + val;
+ }
+ }).join('\n');
+ }
+ } else {
+ insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+ }
+ if (document.queryCommandSupported('insertText')) {
+ inserted = document.execCommand('insertText', false, insertText);
+ }
+ if (!inserted) {
+ try {
+ document.execCommand("ms-beginUndoUnit");
+ } catch (undefined) {}
+ textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+ try {
+ document.execCommand("ms-endUndoUnit");
+ } catch (undefined) {}
+ }
+ return this.moveCursor(textArea, tag, wrap);
+ };
+ gl.text.moveCursor = function(textArea, tag, wrapped) {
+ var pos;
+ if (!textArea.setSelectionRange) {
+ return;
+ }
+ if (textArea.selectionStart === textArea.selectionEnd) {
+ if (wrapped) {
+ pos = textArea.selectionStart - tag.length;
+ } else {
+ pos = textArea.selectionStart;
+ }
+ return textArea.setSelectionRange(pos, pos);
+ }
+ };
+ gl.text.updateText = function(textArea, tag, blockTag, wrap) {
+ var $textArea, oldVal, selected, text;
+ $textArea = $(textArea);
+ oldVal = $textArea.val();
+ textArea = $textArea.get(0);
+ text = $textArea.val();
+ selected = this.selectedText(text, textArea);
+ $textArea.focus();
+ return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+ };
+ gl.text.init = function(form) {
+ var self;
+ self = this;
+ return $('.js-md', form).off('click').on('click', function() {
+ var $this;
+ $this = $(this);
+ return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+ });
+ };
+ gl.text.removeListeners = function(form) {
+ return $('.js-md', form).off();
+ };
+ return gl.text.truncate = function(string, maxLength) {
+ return string.substr(0, (maxLength - 3)) + '...';
+ };
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/text_utility.js.coffee b/app/assets/javascripts/lib/utils/text_utility.js.coffee
deleted file mode 100644
index 2e1407f8738..00000000000
--- a/app/assets/javascripts/lib/utils/text_utility.js.coffee
+++ /dev/null
@@ -1,105 +0,0 @@
-((w) ->
- w.gl ?= {}
- w.gl.text ?= {}
-
- gl.text.randomString = -> Math.random().toString(36).substring(7)
-
- gl.text.replaceRange = (s, start, end, substitute) ->
- s.substring(0, start) + substitute + s.substring(end);
-
- gl.text.selectedText = (text, textarea) ->
- text.substring(textarea.selectionStart, textarea.selectionEnd)
-
- gl.text.lineBefore = (text, textarea) ->
- split = text.substring(0, textarea.selectionStart).trim().split('\n')
- split[split.length - 1]
-
- gl.text.lineAfter = (text, textarea) ->
- text.substring(textarea.selectionEnd).trim().split('\n')[0]
-
- gl.text.blockTagText = (text, textArea, blockTag, selected) ->
- lineBefore = @lineBefore(text, textArea)
- lineAfter = @lineAfter(text, textArea)
-
- if lineBefore is blockTag and lineAfter is blockTag
- # To remove the block tag we have to select the line before & after
- if blockTag?
- textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1)
- textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1)
-
- selected
- else
- "#{blockTag}\n#{selected}\n#{blockTag}"
-
- gl.text.insertText = (textArea, text, tag, blockTag, selected, wrap) ->
- selectedSplit = selected.split('\n')
- startChar = if not wrap and textArea.selectionStart > 0 then '\n' else ''
-
- if selectedSplit.length > 1 and (not wrap or blockTag?)
- if blockTag?
- insertText = @blockTagText(text, textArea, blockTag, selected)
- else
- insertText = selectedSplit.map((val) ->
- if val.indexOf(tag) is 0
- "#{val.replace(tag, '')}"
- else
- "#{tag}#{val}"
- ).join('\n')
- else
- insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}"
-
- if document.queryCommandSupported('insertText')
- inserted = document.execCommand 'insertText', false, insertText
-
- unless inserted
- try
- document.execCommand("ms-beginUndoUnit")
-
- textArea.value = @replaceRange(
- text,
- textArea.selectionStart,
- textArea.selectionEnd,
- insertText)
- try
- document.execCommand("ms-endUndoUnit")
-
- @moveCursor(textArea, tag, wrap)
-
- gl.text.moveCursor = (textArea, tag, wrapped) ->
- return unless textArea.setSelectionRange
-
- if textArea.selectionStart is textArea.selectionEnd
- if wrapped
- pos = textArea.selectionStart - tag.length
- else
- pos = textArea.selectionStart
-
- textArea.setSelectionRange pos, pos
-
- gl.text.updateText = (textArea, tag, blockTag, wrap) ->
- $textArea = $(textArea)
- oldVal = $textArea.val()
- textArea = $textArea.get(0)
- text = $textArea.val()
- selected = @selectedText(text, textArea)
- $textArea.focus()
-
- @insertText(textArea, text, tag, blockTag, selected, wrap)
-
- gl.text.init = (form) ->
- self = @
- $('.js-md', form)
- .off 'click'
- .on 'click', ->
- $this = $(@)
- self.updateText(
- $this.closest('.md-area').find('textarea'),
- $this.data('md-tag'),
- $this.data('md-block'),
- not $this.data('md-prepend')
- )
-
- gl.text.removeListeners = (form) ->
- $('.js-md', form).off()
-
-) window
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
new file mode 100644
index 00000000000..dc30babd645
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -0,0 +1,15 @@
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ return w.gl.utils.isObject = function(obj) {
+ return (obj != null) && (obj.constructor === Object);
+ };
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/type_utility.js.coffee b/app/assets/javascripts/lib/utils/type_utility.js.coffee
deleted file mode 100644
index 957f0d86b36..00000000000
--- a/app/assets/javascripts/lib/utils/type_utility.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-((w) ->
-
- w.gl ?= {}
- w.gl.utils ?= {}
-
- w.gl.utils.isObject = (obj) ->
- obj? and (obj.constructor is Object)
-
-) window
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
new file mode 100644
index 00000000000..533310cc87c
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -0,0 +1,74 @@
+(function() {
+ (function(w) {
+ var base;
+ if (w.gl == null) {
+ w.gl = {};
+ }
+ if ((base = w.gl).utils == null) {
+ base.utils = {};
+ }
+ w.gl.utils.getParameterValues = function(sParam) {
+ var i, sPageURL, sParameterName, sURLVariables, values;
+ sPageURL = decodeURIComponent(window.location.search.substring(1));
+ sURLVariables = sPageURL.split('&');
+ sParameterName = void 0;
+ values = [];
+ i = 0;
+ while (i < sURLVariables.length) {
+ sParameterName = sURLVariables[i].split('=');
+ if (sParameterName[0] === sParam) {
+ values.push(sParameterName[1]);
+ }
+ i++;
+ }
+ return values;
+ };
+ w.gl.utils.mergeUrlParams = function(params, url) {
+ var lastChar, newUrl, paramName, paramValue, pattern;
+ newUrl = decodeURIComponent(url);
+ for (paramName in params) {
+ paramValue = params[paramName];
+ pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
+ if (paramValue == null) {
+ newUrl = newUrl.replace(pattern, '');
+ } else if (url.search(pattern) !== -1) {
+ newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
+ } else {
+ newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
+ }
+ }
+ lastChar = newUrl[newUrl.length - 1];
+ if (lastChar === '&') {
+ newUrl = newUrl.slice(0, -1);
+ }
+ return newUrl;
+ };
+ w.gl.utils.removeParamQueryString = function(url, param) {
+ var urlVariables, variables;
+ url = decodeURIComponent(url);
+ urlVariables = url.split('&');
+ return ((function() {
+ var j, len, results;
+ results = [];
+ for (j = 0, len = urlVariables.length; j < len; j++) {
+ variables = urlVariables[j];
+ if (variables.indexOf(param) === -1) {
+ results.push(variables);
+ }
+ }
+ return results;
+ })()).join('&');
+ };
+ w.gl.utils.getLocationHash = function(url) {
+ var hashIndex;
+ if (typeof url === 'undefined') {
+ // Note: We can't use window.location.hash here because it's
+ // not consistent across browsers - Firefox will pre-decode it
+ url = window.location.href;
+ }
+ hashIndex = url.indexOf('#');
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+ };
+ })(window);
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js.coffee b/app/assets/javascripts/lib/utils/url_utility.js.coffee
deleted file mode 100644
index e8085e1c2e4..00000000000
--- a/app/assets/javascripts/lib/utils/url_utility.js.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-((w) ->
-
- w.gl ?= {}
- w.gl.utils ?= {}
-
- # Returns an array containing the value(s) of the
- # of the key passed as an argument
- w.gl.utils.getParameterValues = (sParam) ->
- sPageURL = decodeURIComponent(window.location.search.substring(1))
- sURLVariables = sPageURL.split('&')
- sParameterName = undefined
- values = []
- i = 0
- while i < sURLVariables.length
- sParameterName = sURLVariables[i].split('=')
- if sParameterName[0] is sParam
- values.push(sParameterName[1])
- i++
- values
-
- # #
- # @param {Object} params - url keys and value to merge
- # @param {String} url
- # #
- w.gl.utils.mergeUrlParams = (params, url) ->
- newUrl = decodeURIComponent(url)
- for paramName, paramValue of params
- pattern = new RegExp "\\b(#{paramName}=).*?(&|$)"
- if not paramValue?
- newUrl = newUrl.replace pattern, ''
- else if url.search(pattern) isnt -1
- newUrl = newUrl.replace pattern, "$1#{paramValue}$2"
- else
- newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
-
- # Remove a trailing ampersand
- lastChar = newUrl[newUrl.length - 1]
-
- if lastChar is '&'
- newUrl = newUrl.slice 0, -1
-
- newUrl
-
- # removes parameter query string from url. returns the modified url
- w.gl.utils.removeParamQueryString = (url, param) ->
- url = decodeURIComponent(url)
- urlVariables = url.split('&')
- (
- variables for variables in urlVariables when variables.indexOf(param) is -1
- ).join('&')
-
-) window
diff --git a/app/assets/javascripts/lib/utils/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js
deleted file mode 100644
index 39ffe44dae0..00000000000
--- a/app/assets/javascripts/lib/utils/utf8_encode.js
+++ /dev/null
@@ -1,70 +0,0 @@
-function utf8_encode (argString) {
- // http://kevin.vanzonneveld.net
- // + original by: Webtoolkit.info (http://www.webtoolkit.info/)
- // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
- // + improved by: sowberry
- // + tweaked by: Jack
- // + bugfixed by: Onno Marsman
- // + improved by: Yves Sucaet
- // + bugfixed by: Onno Marsman
- // + bugfixed by: Ulrich
- // + bugfixed by: Rafal Kukawski
- // + improved by: kirilloid
- // + bugfixed by: kirilloid
- // * example 1: utf8_encode('Kevin van Zonneveld');
- // * returns 1: 'Kevin van Zonneveld'
-
- if (argString === null || typeof argString === "undefined") {
- return "";
- }
-
- var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n");
- var utftext = '',
- start, end, stringl = 0;
-
- start = end = 0;
- stringl = string.length;
- for (var n = 0; n < stringl; n++) {
- var c1 = string.charCodeAt(n);
- var enc = null;
-
- if (c1 < 128) {
- end++;
- } else if (c1 > 127 && c1 < 2048) {
- enc = String.fromCharCode(
- (c1 >> 6) | 192,
- ( c1 & 63) | 128
- );
- } else if (c1 & 0xF800 != 0xD800) {
- enc = String.fromCharCode(
- (c1 >> 12) | 224,
- ((c1 >> 6) & 63) | 128,
- ( c1 & 63) | 128
- );
- } else { // surrogate pairs
- if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); }
- var c2 = string.charCodeAt(++n);
- if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); }
- c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000;
- enc = String.fromCharCode(
- (c1 >> 18) | 240,
- ((c1 >> 12) & 63) | 128,
- ((c1 >> 6) & 63) | 128,
- ( c1 & 63) | 128
- );
- }
- if (enc !== null) {
- if (end > start) {
- utftext += string.slice(start, end);
- }
- utftext += enc;
- start = end = n + 1;
- }
- }
-
- if (end > start) {
- utftext += string.slice(start, stringl);
- }
-
- return utftext;
-}
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
new file mode 100644
index 00000000000..f145bd3ad74
--- /dev/null
+++ b/app/assets/javascripts/line_highlighter.js
@@ -0,0 +1,115 @@
+
+/*= require jquery.scrollTo */
+
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.LineHighlighter = (function() {
+ LineHighlighter.prototype.highlightClass = 'hll';
+
+ LineHighlighter.prototype._hash = '';
+
+ function LineHighlighter(hash) {
+ var range;
+ if (hash == null) {
+ hash = location.hash;
+ }
+ this.setHash = bind(this.setHash, this);
+ this.highlightLine = bind(this.highlightLine, this);
+ this.clickHandler = bind(this.clickHandler, this);
+ this._hash = hash;
+ this.bindEvents();
+ if (hash !== '') {
+ range = this.hashToRange(hash);
+ if (range[0]) {
+ this.highlightRange(range);
+ $.scrollTo("#L" + range[0], {
+ offset: -150
+ });
+ }
+ }
+ }
+
+ LineHighlighter.prototype.bindEvents = function() {
+ $('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
+ return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
+ return event.preventDefault();
+ });
+ };
+
+ LineHighlighter.prototype.clickHandler = function(event) {
+ var current, lineNumber, range;
+ event.preventDefault();
+ this.clearHighlight();
+ lineNumber = $(event.target).closest('a').data('line-number');
+ current = this.hashToRange(this._hash);
+ if (!(current[0] && event.shiftKey)) {
+ this.setHash(lineNumber);
+ return this.highlightLine(lineNumber);
+ } else if (event.shiftKey) {
+ if (lineNumber < current[0]) {
+ range = [lineNumber, current[0]];
+ } else {
+ range = [current[0], lineNumber];
+ }
+ this.setHash(range[0], range[1]);
+ return this.highlightRange(range);
+ }
+ };
+
+ LineHighlighter.prototype.clearHighlight = function() {
+ return $("." + this.highlightClass).removeClass(this.highlightClass);
+ };
+
+ LineHighlighter.prototype.hashToRange = function(hash) {
+ var first, last, matches;
+ matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
+ if (matches && matches.length) {
+ first = parseInt(matches[1]);
+ last = matches[2] ? parseInt(matches[2]) : null;
+ return [first, last];
+ } else {
+ return [null, null];
+ }
+ };
+
+ LineHighlighter.prototype.highlightLine = function(lineNumber) {
+ return $("#LC" + lineNumber).addClass(this.highlightClass);
+ };
+
+ LineHighlighter.prototype.highlightRange = function(range) {
+ var i, lineNumber, ref, ref1, results;
+ if (range[1]) {
+ results = [];
+ for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? ++i : --i) {
+ results.push(this.highlightLine(lineNumber));
+ }
+ return results;
+ } else {
+ return this.highlightLine(range[0]);
+ }
+ };
+
+ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
+ var hash;
+ if (lastLineNumber) {
+ hash = "#L" + firstLineNumber + "-" + lastLineNumber;
+ } else {
+ hash = "#L" + firstLineNumber;
+ }
+ this._hash = hash;
+ return this.__setLocationHash__(hash);
+ };
+
+ LineHighlighter.prototype.__setLocationHash__ = function(value) {
+ return history.pushState({
+ turbolinks: false,
+ url: value
+ }, document.title, value);
+ };
+
+ return LineHighlighter;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/line_highlighter.js.coffee b/app/assets/javascripts/line_highlighter.js.coffee
deleted file mode 100644
index 2254a3f91ae..00000000000
--- a/app/assets/javascripts/line_highlighter.js.coffee
+++ /dev/null
@@ -1,148 +0,0 @@
-# LineHighlighter
-#
-# Handles single- and multi-line selection and highlight for blob views.
-#
-#= require jquery.scrollTo
-#
-# ### Example Markup
-#
-# <div id="blob-content-holder">
-# <div class="file-content">
-# <div class="line-numbers">
-# <a href="#L1" id="L1" data-line-number="1">1</a>
-# <a href="#L2" id="L2" data-line-number="2">2</a>
-# <a href="#L3" id="L3" data-line-number="3">3</a>
-# <a href="#L4" id="L4" data-line-number="4">4</a>
-# <a href="#L5" id="L5" data-line-number="5">5</a>
-# </div>
-# <pre class="code highlight">
-# <code>
-# <span id="LC1" class="line">...</span>
-# <span id="LC2" class="line">...</span>
-# <span id="LC3" class="line">...</span>
-# <span id="LC4" class="line">...</span>
-# <span id="LC5" class="line">...</span>
-# </code>
-# </pre>
-# </div>
-# </div>
-#
-class @LineHighlighter
- # CSS class applied to highlighted lines
- highlightClass: 'hll'
-
- # Internal copy of location.hash so we're not dependent on `location` in tests
- _hash: ''
-
- # Initialize a LineHighlighter object
- #
- # hash - String URL hash for dependency injection in tests
- constructor: (hash = location.hash) ->
- @_hash = hash
-
- @bindEvents()
-
- unless hash == ''
- range = @hashToRange(hash)
-
- if range[0]
- @highlightRange(range)
-
- # Scroll to the first highlighted line on initial load
- # Offset -50 for the sticky top bar, and another -100 for some context
- $.scrollTo("#L#{range[0]}", offset: -150)
-
- bindEvents: ->
- $('#blob-content-holder').on 'mousedown', 'a[data-line-number]', @clickHandler
-
- # While it may seem odd to bind to the mousedown event and then throw away
- # the click event, there is a method to our madness.
- #
- # If not done this way, the line number anchor will sometimes keep its
- # active state even when the event is cancelled, resulting in an ugly border
- # around the link and/or a persisted underline text decoration.
-
- $('#blob-content-holder').on 'click', 'a[data-line-number]', (event) ->
- event.preventDefault()
-
- clickHandler: (event) =>
- event.preventDefault()
-
- @clearHighlight()
-
- lineNumber = $(event.target).closest('a').data('line-number')
- current = @hashToRange(@_hash)
-
- unless current[0] && event.shiftKey
- # If there's no current selection, or there is but Shift wasn't held,
- # treat this like a single-line selection.
- @setHash(lineNumber)
- @highlightLine(lineNumber)
- else if event.shiftKey
- if lineNumber < current[0]
- range = [lineNumber, current[0]]
- else
- range = [current[0], lineNumber]
-
- @setHash(range[0], range[1])
- @highlightRange(range)
-
- # Unhighlight previously highlighted lines
- clearHighlight: ->
- $(".#{@highlightClass}").removeClass(@highlightClass)
-
- # Convert a URL hash String into line numbers
- #
- # hash - Hash String
- #
- # Examples:
- #
- # hashToRange('#L5') # => [5, null]
- # hashToRange('#L5-15') # => [5, 15]
- # hashToRange('#foo') # => [null, null]
- #
- # Returns an Array
- hashToRange: (hash) ->
- matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/)
-
- if matches && matches.length
- first = parseInt(matches[1])
- last = if matches[2] then parseInt(matches[2]) else null
-
- [first, last]
- else
- [null, null]
-
- # Highlight a single line
- #
- # lineNumber - Line number to highlight
- highlightLine: (lineNumber) =>
- $("#LC#{lineNumber}").addClass(@highlightClass)
-
- # Highlight all lines within a range
- #
- # range - Array containing the starting and ending line numbers
- highlightRange: (range) ->
- if range[1]
- for lineNumber in [range[0]..range[1]]
- @highlightLine(lineNumber)
- else
- @highlightLine(range[0])
-
- # Set the URL hash string
- setHash: (firstLineNumber, lastLineNumber) =>
- if lastLineNumber
- hash = "#L#{firstLineNumber}-#{lastLineNumber}"
- else
- hash = "#L#{firstLineNumber}"
-
- @_hash = hash
- @__setLocationHash__(hash)
-
- # Make the actual hash change in the browser
- #
- # This method is stubbed in tests.
- __setLocationHash__: (value) ->
- # We're using pushState instead of assigning location.hash directly to
- # prevent the page from scrolling on the hashchange event
- history.pushState({turbolinks: false, url: value}, document.title, value)
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
new file mode 100644
index 00000000000..e5d4fd44c96
--- /dev/null
+++ b/app/assets/javascripts/logo.js
@@ -0,0 +1,16 @@
+(function() {
+ Turbolinks.enableProgressBar();
+
+ start = function() {
+ $('.tanuki-logo').addClass('animate');
+ };
+
+ stop = function() {
+ $('.tanuki-logo').removeClass('animate');
+ };
+
+ $(document).on('page:fetch', start);
+
+ $(document).on('page:change', stop);
+
+}).call(this);
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
deleted file mode 100644
index dc2590a0355..00000000000
--- a/app/assets/javascripts/logo.js.coffee
+++ /dev/null
@@ -1,44 +0,0 @@
-Turbolinks.enableProgressBar();
-
-defaultClass = 'tanuki-shape'
-pieces = [
- 'path#tanuki-right-cheek',
- 'path#tanuki-right-eye, path#tanuki-right-ear',
- 'path#tanuki-nose',
- 'path#tanuki-left-eye, path#tanuki-left-ear',
- 'path#tanuki-left-cheek',
-]
-pieceIndex = 0
-firstPiece = pieces[0]
-
-currentTimer = null
-delay = 150
-
-clearHighlights = ->
- $(".#{defaultClass}.highlight").attr('class', defaultClass)
-
-start = ->
- clearHighlights()
- pieceIndex = 0
- pieces.reverse() unless pieces[0] == firstPiece
- clearInterval(currentTimer) if currentTimer
- currentTimer = setInterval(work, delay)
-
-stop = ->
- clearInterval(currentTimer)
- clearHighlights()
-
-work = ->
- clearHighlights()
- $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
-
- # If we hit the last piece, reset the index and then reverse the array to
- # get a nice back-and-forth sweeping look
- if pieceIndex == pieces.length - 1
- pieceIndex = 0
- pieces.reverse()
- else
- pieceIndex++
-
-$(document).on('page:fetch', start)
-$(document).on('page:change', stop)
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
deleted file mode 100644
index 2a0b9479445..00000000000
--- a/app/assets/javascripts/markdown_preview.js.coffee
+++ /dev/null
@@ -1,119 +0,0 @@
-# MarkdownPreview
-#
-# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
-# and showing a warning when more than `x` users are referenced.
-#
-class @MarkdownPreview
- # Minimum number of users referenced before triggering a warning
- referenceThreshold: 10
- ajaxCache: {}
-
- showPreview: (form) ->
- preview = form.find('.js-md-preview')
- mdText = form.find('textarea.markdown-area').val()
-
- if mdText.trim().length == 0
- preview.text('Nothing to preview.')
- @hideReferencedUsers(form)
- else
- preview.text('Loading...')
- @renderMarkdown mdText, (response) =>
- preview.html(response.body)
- preview.syntaxHighlight()
- @renderReferencedUsers(response.references.users, form)
-
- renderMarkdown: (text, success) ->
- return unless window.markdown_preview_path
-
- return success(@ajaxCache.response) if text == @ajaxCache.text
-
- $.ajax
- type: 'POST'
- url: window.markdown_preview_path
- data: { text: text }
- dataType: 'json'
- success: (response) =>
- @ajaxCache = text: text, response: response
- success(response)
-
- hideReferencedUsers: (form) ->
- referencedUsers = form.find('.referenced-users')
- referencedUsers.hide()
-
- renderReferencedUsers: (users, form) ->
- referencedUsers = form.find('.referenced-users')
-
- if referencedUsers.length
- if users.length >= @referenceThreshold
- referencedUsers.show()
- referencedUsers.find('.js-referenced-users-count').text(users.length)
- else
- referencedUsers.hide()
-
-markdownPreview = new MarkdownPreview()
-
-previewButtonSelector = '.js-md-preview-button'
-writeButtonSelector = '.js-md-write-button'
-lastTextareaPreviewed = null
-
-$.fn.setupMarkdownPreview = ->
- $form = $(this)
-
- form_textarea = $form.find('textarea.markdown-area')
-
- form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
- form_textarea.on 'blur', -> markdownPreview.showPreview($form)
-
-$(document).on 'markdown-preview:show', (e, $form) ->
- return unless $form
-
- lastTextareaPreviewed = $form.find('textarea.markdown-area')
-
- # toggle tabs
- $form.find(writeButtonSelector).parent().removeClass('active')
- $form.find(previewButtonSelector).parent().addClass('active')
-
- # toggle content
- $form.find('.md-write-holder').hide()
- $form.find('.md-preview-holder').show()
-
- markdownPreview.showPreview($form)
-
-$(document).on 'markdown-preview:hide', (e, $form) ->
- return unless $form
-
- lastTextareaPreviewed = null
-
- # toggle tabs
- $form.find(writeButtonSelector).parent().addClass('active')
- $form.find(previewButtonSelector).parent().removeClass('active')
-
- # toggle content
- $form.find('.md-write-holder').show()
- $form.find('textarea.markdown-area').focus()
- $form.find('.md-preview-holder').hide()
-
-$(document).on 'markdown-preview:toggle', (e, keyboardEvent) ->
- $target = $(keyboardEvent.target)
-
- if $target.is('textarea.markdown-area')
- $(document).triggerHandler('markdown-preview:show', [$target.closest('form')])
- keyboardEvent.preventDefault()
- else if lastTextareaPreviewed
- $target = lastTextareaPreviewed
- $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')])
- keyboardEvent.preventDefault()
-
-$(document).on 'click', previewButtonSelector, (e) ->
- e.preventDefault()
-
- $form = $(this).closest('form')
-
- $(document).triggerHandler('markdown-preview:show', [$form])
-
-$(document).on 'click', writeButtonSelector, (e) ->
- e.preventDefault()
-
- $form = $(this).closest('form')
-
- $(document).triggerHandler('markdown-preview:hide', [$form])
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..1935af491f7
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(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
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ gl.MemberExpirationDate = function() {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+
+ var inputs = $('.js-access-expiration-date');
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: toggleClearInput
+ });
+
+ inputs.next('.js-clear-input').on('click', function(event) {
+ event.preventDefault();
+
+ var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ input.datepicker('setDate', null);
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
new file mode 100644
index 00000000000..cd92df8ddc5
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -0,0 +1,341 @@
+const HEAD_HEADER_TEXT = 'HEAD//our changes';
+const ORIGIN_HEADER_TEXT = 'origin//their changes';
+const HEAD_BUTTON_TITLE = 'Use ours';
+const ORIGIN_BUTTON_TITLE = 'Use theirs';
+
+
+class MergeConflictDataProvider {
+
+ getInitialData() {
+ const diffViewType = $.cookie('diff_view');
+
+ return {
+ isLoading : true,
+ hasError : false,
+ isParallel : diffViewType === 'parallel',
+ diffViewType : diffViewType,
+ isSubmitting : false,
+ conflictsData : {},
+ resolutionData : {}
+ }
+ }
+
+
+ decorateData(vueInstance, data) {
+ this.vueInstance = vueInstance;
+
+ if (data.type === 'error') {
+ vueInstance.hasError = true;
+ data.errorMessage = data.message;
+ }
+ else {
+ data.shortCommitSha = data.commit_sha.slice(0, 7);
+ data.commitMessage = data.commit_message;
+
+ this.setParallelLines(data);
+ this.setInlineLines(data);
+ this.updateResolutionsData(data);
+ }
+
+ vueInstance.conflictsData = data;
+ vueInstance.isSubmitting = false;
+
+ const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict';
+ vueInstance.conflictsData.conflictsText = conflictsText;
+ }
+
+
+ updateResolutionsData(data) {
+ const vi = this.vueInstance;
+
+ data.files.forEach( (file) => {
+ file.sections.forEach( (section) => {
+ if (section.conflict) {
+ vi.$set(`resolutionData['${section.id}']`, false);
+ }
+ });
+ });
+ }
+
+
+ setParallelLines(data) {
+ data.files.forEach( (file) => {
+ file.filePath = this.getFilePath(file);
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.parallelLines = [];
+ const linesObj = { left: [], right: [] };
+
+ file.sections.forEach( (section) => {
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ linesObj.left.push(this.getOriginHeaderLine(id));
+ linesObj.right.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if (conflict) {
+ if (type === 'old') {
+ linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
+ }
+ else if (type === 'new') {
+ linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
+ }
+ }
+ else {
+ const lineType = type || 'context';
+
+ linesObj.left.push (this.getLineForParallelView(line, id, lineType));
+ linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
+ }
+ });
+
+ this.checkLineLengths(linesObj);
+ });
+
+ for (let i = 0, len = linesObj.left.length; i < len; i++) {
+ file.parallelLines.push([
+ linesObj.right[i],
+ linesObj.left[i]
+ ]);
+ }
+
+ });
+ }
+
+
+ checkLineLengths(linesObj) {
+ let { left, right } = linesObj;
+
+ if (left.length !== right.length) {
+ if (left.length > right.length) {
+ const diff = left.length - right.length;
+ for (let i = 0; i < diff; i++) {
+ right.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ else {
+ const diff = right.length - left.length;
+ for (let i = 0; i < diff; i++) {
+ left.push({ lineType: 'emptyLine', richText: '' });
+ }
+ }
+ }
+ }
+
+
+ setInlineLines(data) {
+ data.files.forEach( (file) => {
+ file.iconClass = `fa-${file.blob_icon}`;
+ file.blobPath = file.blob_path;
+ file.filePath = this.getFilePath(file);
+ file.inlineLines = []
+
+ file.sections.forEach( (section) => {
+ let currentLineType = 'new';
+ const { conflict, lines, id } = section;
+
+ if (conflict) {
+ file.inlineLines.push(this.getHeadHeaderLine(id));
+ }
+
+ lines.forEach( (line) => {
+ const { type } = line;
+
+ if ((type === 'new' || type === 'old') && currentLineType !== type) {
+ currentLineType = type;
+ file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
+ }
+
+ this.decorateLineForInlineView(line, id, conflict);
+ file.inlineLines.push(line);
+ })
+
+ if (conflict) {
+ file.inlineLines.push(this.getOriginHeaderLine(id));
+ }
+ });
+ });
+ }
+
+
+ handleSelected(sectionId, selection) {
+ const vi = this.vueInstance;
+
+ vi.resolutionData[sectionId] = selection;
+ vi.conflictsData.files.forEach( (file) => {
+ file.inlineLines.forEach( (line) => {
+ if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
+ this.markLine(line, selection);
+ }
+ });
+
+ file.parallelLines.forEach( (lines) => {
+ const left = lines[0];
+ const right = lines[1];
+ const hasSameId = right.id === sectionId || left.id === sectionId;
+ const isLeftMatch = left.hasConflict || left.isHeader;
+ const isRightMatch = right.hasConflict || right.isHeader;
+
+ if (hasSameId && (isLeftMatch || isRightMatch)) {
+ this.markLine(left, selection);
+ this.markLine(right, selection);
+ }
+ })
+ });
+ }
+
+
+ updateViewType(newType) {
+ const vi = this.vueInstance;
+
+ if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
+ return;
+ }
+
+ vi.diffView = newType;
+ vi.isParallel = newType === 'parallel';
+ $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
+ $('.content-wrapper .container-fluid').toggleClass('container-limited');
+ }
+
+
+ markLine(line, selection) {
+ if (selection === 'head' && line.isHead) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else if (selection === 'origin' && line.isOrigin) {
+ line.isSelected = true;
+ line.isUnselected = false;
+ }
+ else {
+ line.isSelected = false;
+ line.isUnselected = true;
+ }
+ }
+
+
+ getConflictsCount() {
+ return Object.keys(this.vueInstance.resolutionData).length;
+ }
+
+
+ getResolvedCount() {
+ let count = 0;
+ const data = this.vueInstance.resolutionData;
+
+ for (const id in data) {
+ const resolution = data[id];
+ if (resolution) {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+
+ isReadyToCommit() {
+ const { conflictsData, isSubmitting } = this.vueInstance
+ const allResolved = this.getConflictsCount() === this.getResolvedCount();
+ const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
+
+ return !isSubmitting && hasCommitMessage && allResolved;
+ }
+
+
+ getCommitButtonText() {
+ const initial = 'Commit conflict resolution';
+ const inProgress = 'Committing...';
+ const vue = this.vueInstance;
+
+ return vue ? vue.isSubmitting ? inProgress : initial : initial;
+ }
+
+
+ decorateLineForInlineView(line, id, conflict) {
+ const { type } = line;
+ line.id = id;
+ line.hasConflict = conflict;
+ line.isHead = type === 'new';
+ line.isOrigin = type === 'old';
+ line.hasMatch = type === 'match';
+ line.richText = line.rich_text;
+ line.isSelected = false;
+ line.isUnselected = false;
+ }
+
+ getLineForParallelView(line, id, lineType, isHead) {
+ const { old_line, new_line, rich_text } = line;
+ const hasConflict = lineType === 'conflict';
+
+ return {
+ id,
+ lineType,
+ hasConflict,
+ isHead : hasConflict && isHead,
+ isOrigin : hasConflict && !isHead,
+ hasMatch : lineType === 'match',
+ lineNumber : isHead ? new_line : old_line,
+ section : isHead ? 'head' : 'origin',
+ richText : rich_text,
+ isSelected : false,
+ isUnselected : false
+ }
+ }
+
+
+ getHeadHeaderLine(id) {
+ return {
+ id : id,
+ richText : HEAD_HEADER_TEXT,
+ buttonTitle : HEAD_BUTTON_TITLE,
+ type : 'new',
+ section : 'head',
+ isHeader : true,
+ isHead : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ getOriginHeaderLine(id) {
+ return {
+ id : id,
+ richText : ORIGIN_HEADER_TEXT,
+ buttonTitle : ORIGIN_BUTTON_TITLE,
+ type : 'old',
+ section : 'origin',
+ isHeader : true,
+ isOrigin : true,
+ isSelected : false,
+ isUnselected: false
+ }
+ }
+
+
+ handleFailedRequest(vueInstance, data) {
+ vueInstance.hasError = true;
+ vueInstance.conflictsData.errorMessage = 'Something went wrong!';
+ }
+
+
+ getCommitData() {
+ return {
+ commit_message: this.vueInstance.conflictsData.commitMessage,
+ sections: this.vueInstance.resolutionData
+ }
+ }
+
+
+ getFilePath(file) {
+ const { old_path, new_path } = file;
+ return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
+ }
+
+}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
new file mode 100644
index 00000000000..b56fd5aa658
--- /dev/null
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -0,0 +1,83 @@
+//= require vue
+
+class MergeConflictResolver {
+
+ constructor() {
+ this.dataProvider = new MergeConflictDataProvider()
+ this.initVue()
+ }
+
+
+ initVue() {
+ const that = this;
+ this.vue = new Vue({
+ el : '#conflicts',
+ name : 'MergeConflictResolver',
+ data : this.dataProvider.getInitialData(),
+ created : this.fetchData(),
+ computed : this.setComputedProperties(),
+ methods : {
+ handleSelected(sectionId, selection) {
+ that.dataProvider.handleSelected(sectionId, selection);
+ },
+ handleViewTypeChange(newType) {
+ that.dataProvider.updateViewType(newType);
+ },
+ commit() {
+ that.commit();
+ }
+ }
+ })
+ }
+
+
+ setComputedProperties() {
+ const dp = this.dataProvider;
+
+ return {
+ conflictsCount() { return dp.getConflictsCount() },
+ resolvedCount() { return dp.getResolvedCount() },
+ readyToCommit() { return dp.isReadyToCommit() },
+ commitButtonText() { return dp.getCommitButtonText() }
+ }
+ }
+
+
+ fetchData() {
+ const dp = this.dataProvider;
+
+ $.get($('#conflicts').data('conflictsPath'))
+ .done((data) => {
+ dp.decorateData(this.vue, data);
+ })
+ .error((data) => {
+ dp.handleFailedRequest(this.vue, data);
+ })
+ .always(() => {
+ this.vue.isLoading = false;
+
+ this.vue.$nextTick(() => {
+ $('#conflicts .js-syntax-highlight').syntaxHighlight();
+ });
+
+ if (this.vue.diffViewType === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
+ })
+ }
+
+
+ commit() {
+ this.vue.isSubmitting = true;
+
+ $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ this.vue.isSubmitting = false;
+ new Flash('Something went wrong!');
+ });
+ }
+
+}
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
new file mode 100644
index 00000000000..56ebf84c4f6
--- /dev/null
+++ b/app/assets/javascripts/merge_request.js
@@ -0,0 +1,105 @@
+
+/*= require jquery.waitforimages */
+
+
+/*= require task_list */
+
+
+/*= require merge_request_tabs */
+
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.MergeRequest = (function() {
+ function MergeRequest(opts) {
+ this.opts = opts != null ? opts : {};
+ this.submitNoteForm = bind(this.submitNoteForm, this);
+ this.$el = $('.merge-request');
+ this.$('.show-all-commits').on('click', (function(_this) {
+ return function() {
+ return _this.showAllCommits();
+ };
+ })(this));
+ this.initTabs();
+ this.disableTaskList();
+ this.initMRBtnListeners();
+ if ($("a.btn-close").length) {
+ this.initTaskList();
+ }
+ }
+
+ MergeRequest.prototype.$ = function(selector) {
+ return this.$el.find(selector);
+ };
+
+ MergeRequest.prototype.initTabs = function() {
+ if (this.opts.action !== 'new') {
+ window.mrTabs = new MergeRequestTabs(this.opts);
+ } else {
+ return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
+ }
+ };
+
+ MergeRequest.prototype.showAllCommits = function() {
+ this.$('.first-commits').remove();
+ return this.$('.all-commits').removeClass('hide');
+ };
+
+ MergeRequest.prototype.initTaskList = function() {
+ $('.detail-page-description .js-task-list-container').taskList('enable');
+ return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
+ };
+
+ MergeRequest.prototype.initMRBtnListeners = function() {
+ var _this;
+ _this = this;
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, shouldSubmit;
+ $this = $(this);
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit && $this.data('submitted')) {
+ return;
+ }
+ if (shouldSubmit) {
+ if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ return _this.submitNoteForm($this.closest('form'), $this);
+ }
+ }
+ });
+ };
+
+ MergeRequest.prototype.submitNoteForm = function(form, $button) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ form.submit();
+ $button.data('submitted', true);
+ return $button.trigger('click');
+ }
+ };
+
+ MergeRequest.prototype.disableTaskList = function() {
+ $('.detail-page-description .js-task-list-container').taskList('disable');
+ return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
+ };
+
+ MergeRequest.prototype.updateTaskList = function() {
+ var patchData;
+ patchData = {};
+ patchData['merge_request'] = {
+ 'description': $('.js-task-list-field', this).val()
+ };
+ return $.ajax({
+ type: 'PATCH',
+ url: $('form.js-issuable-update').attr('action'),
+ data: patchData
+ });
+ };
+
+ return MergeRequest;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
deleted file mode 100644
index dabfd91cf14..00000000000
--- a/app/assets/javascripts/merge_request.js.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-#= require jquery.waitforimages
-#= require task_list
-
-#= require merge_request_tabs
-
-class @MergeRequest
- # Initialize MergeRequest behavior
- #
- # Options:
- # action - String, current controller action
- #
- constructor: (@opts = {}) ->
- this.$el = $('.merge-request')
-
- this.$('.show-all-commits').on 'click', =>
- this.showAllCommits()
-
- @initTabs()
-
- # Prevent duplicate event bindings
- @disableTaskList()
- @initMRBtnListeners()
-
- if $("a.btn-close").length
- @initTaskList()
-
- # Local jQuery finder
- $: (selector) ->
- this.$el.find(selector)
-
- initTabs: ->
- if @opts.action != 'new'
- # `MergeRequests#new` has no tab-persisting or lazy-loading behavior
- new MergeRequestTabs(@opts)
- else
- # Show the first tab (Commits)
- $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show')
-
- showAllCommits: ->
- this.$('.first-commits').remove()
- this.$('.all-commits').removeClass 'hide'
-
- initTaskList: ->
- $('.detail-page-description .js-task-list-container').taskList('enable')
- $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
-
- initMRBtnListeners: ->
- _this = @
- $('a.btn-close, a.btn-reopen').on 'click', (e) ->
- $this = $(this)
- shouldSubmit = $this.hasClass('btn-comment')
- if shouldSubmit && $this.data('submitted')
- return
- if shouldSubmit
- if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
- e.preventDefault()
- e.stopImmediatePropagation()
- _this.submitNoteForm($this.closest('form'),$this)
-
-
- submitNoteForm: (form, $button) =>
- noteText = form.find("textarea.js-note-text").val()
- if noteText.trim().length > 0
- form.submit()
- $button.data('submitted',true)
- $button.trigger('click')
-
-
- disableTaskList: ->
- $('.detail-page-description .js-task-list-container').taskList('disable')
- $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
-
- # TODO (rspeicher): Make the merge request description inline-editable like a
- # note so that we can re-use its form here
- updateTaskList: ->
- patchData = {}
- patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()}
-
- $.ajax
- type: 'PATCH'
- url: $('form.js-issuable-update').attr('action')
- data: patchData
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
new file mode 100644
index 00000000000..ad08209d61e
--- /dev/null
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -0,0 +1,268 @@
+
+/*= require jquery.cookie */
+
+(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;
+
+ function MergeRequestTabs(opts) {
+ this.opts = opts != null ? opts : {};
+ this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
+ this.setCurrentAction = bind(this.setCurrentAction, this);
+ this.tabShown = bind(this.tabShown, this);
+ this.showTab = bind(this.showTab, this);
+ this._location = location;
+ this.bindEvents();
+ this.activateTab(this.opts.action);
+ }
+
+ MergeRequestTabs.prototype.bindEvents = function() {
+ $(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
+ return $(document).on('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();
+ } else if (action === 'diffs') {
+ this.loadDiff($target.attr('href'));
+ if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
+ this.shrinkView();
+ }
+ navBarHeight = $('.navbar-gitlab').outerHeight();
+ $.scrollTo(".merge-request-details .merge-request-tabs", {
+ offset: -navBarHeight
+ });
+ } else if (action === 'builds') {
+ this.loadBuilds($target.attr('href'));
+ this.expandView();
+ } else if (action === 'pipelines') {
+ this.loadPipelines($target.attr('href'));
+ this.expandView();
+ } else {
+ this.expandView();
+ }
+ 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
+ });
+ }
+ }
+ };
+
+ MergeRequestTabs.prototype.activateTab = function(action) {
+ if (action === 'show') {
+ action = 'notes';
+ }
+ return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
+ };
+
+ MergeRequestTabs.prototype.setCurrentAction = function(action) {
+ var new_state;
+ if (action === 'show') {
+ action = 'notes';
+ }
+ this.currentAction = action;
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ if (action !== 'notes') {
+ new_state += "/" + action;
+ }
+ new_state += this._location.search + this._location.hash;
+ history.replaceState({
+ turbolinks: true,
+ url: new_state
+ }, 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;
+ }
+ return this._get({
+ url: (source + ".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.expandViewContainer();
+ }
+ _this.diffsLoaded = true;
+ _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.highlighSelectedLine = function() {
+ var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
+ $('.hll').removeClass('hll');
+ locationHash = window.location.hash;
+ if (locationHash !== '') {
+ hashClassString = "." + (locationHash.replace('#', ''));
+ $diffLine = $(locationHash + ":not(.match)", $('#diffs'));
+ if (!$diffLine.is('tr')) {
+ $diffLine = $('#diffs').find("td" + locationHash + ", td" + hashClassString);
+ } 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;
+ 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)
+ });
+ };
+
+ 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.expandViewContainer = function() {
+ return $('.container-fluid').removeClass('container-limited');
+ };
+
+ 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]);
+ }
+ }, 0);
+ };
+
+ MergeRequestTabs.prototype.expandView = function() {
+ var $gutterIcon;
+ if ($.cookie('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);
+ };
+
+ return MergeRequestTabs;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
deleted file mode 100644
index 894f80586f1..00000000000
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ /dev/null
@@ -1,251 +0,0 @@
-# MergeRequestTabs
-#
-# Handles persisting and restoring the current tab selection and lazily-loading
-# content on the MergeRequests#show page.
-#
-#= require jquery.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>
-#
-class @MergeRequestTabs
- diffsLoaded: false
- buildsLoaded: false
- commitsLoaded: false
-
- constructor: (@opts = {}) ->
- # Store the `location` object, allowing for easier stubbing in tests
- @_location = location
-
- @bindEvents()
- @activateTab(@opts.action)
-
- bindEvents: ->
- $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown
- $(document).on 'click', '.js-show-tab', @showTab
-
- showTab: (event) =>
- event.preventDefault()
-
- @activateTab $(event.target).data('action')
-
- tabShown: (event) =>
- $target = $(event.target)
- action = $target.data('action')
-
- if action == 'commits'
- @loadCommits($target.attr('href'))
- @expandView()
- else if action == 'diffs'
- @loadDiff($target.attr('href'))
- if bp? and bp.getBreakpointSize() isnt 'lg'
- @shrinkView()
-
- navBarHeight = $('.navbar-gitlab').outerHeight()
- $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight)
- else if action == 'builds'
- @loadBuilds($target.attr('href'))
- @expandView()
- else
- @expandView()
-
- @setCurrentAction(action)
-
- scrollToElement: (container) ->
- if window.location.hash
- navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight()
-
- $el = $("#{container} #{window.location.hash}:not(.match)")
- $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length
-
- # Activate a tab based on the current action
- activateTab: (action) ->
- action = 'notes' if action == 'show'
- $(".merge-request-tabs a[data-action='#{action}']").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) =>
- # Normalize action, just to be safe
- action = 'notes' if action == 'show'
-
- # Remove a trailing '/commits' or '/diffs'
- new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '')
-
- # Append the new action if we're on a tab other than 'notes'
- unless action == 'notes'
- new_state += "/#{action}"
-
- # Ensure parameters and hash come along for the ride
- new_state += @_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
- history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
-
- new_state
-
- loadCommits: (source) ->
- return if @commitsLoaded
-
- @_get
- url: "#{source}.json"
- success: (data) =>
- document.querySelector("div#commits").innerHTML = data.html
- gl.utils.localTimeAgo($('.js-timeago', 'div#commits'))
- @commitsLoaded = true
- @scrollToElement("#commits")
-
- loadDiff: (source) ->
- return if @diffsLoaded
-
- @_get
- url: "#{source}.json" + @_location.search
- success: (data) =>
- $('#diffs').html data.html
- gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'))
- $('#diffs .js-syntax-highlight').syntaxHighlight()
- @expandViewContainer() if @diffViewType() is 'parallel'
- @diffsLoaded = true
- @scrollToElement("#diffs")
- @highlighSelectedLine()
-
- $(document)
- .off 'click', '.diff-line-num a'
- .on 'click', '.diff-line-num a', (e) =>
- e.preventDefault()
- window.location.hash = $(e.currentTarget).attr 'href'
- @highlighSelectedLine()
- @scrollToElement("#diffs")
-
- highlighSelectedLine: ->
- $('.hll').removeClass 'hll'
- locationHash = window.location.hash
-
- if locationHash isnt ''
- hashClassString = ".#{locationHash.replace('#', '')}"
- $diffLine = $("#{locationHash}:not(.match)", $('#diffs'))
-
- if not $diffLine.is 'tr'
- $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}")
- else
- $diffLine = $diffLine.find('td')
-
- if $diffLine.length
- $diffLine.addClass 'hll'
- diffLineTop = $diffLine.offset().top
- navBarHeight = $('.navbar-gitlab').outerHeight()
-
- loadBuilds: (source) ->
- return if @buildsLoaded
-
- @_get
- url: "#{source}.json"
- success: (data) =>
- document.querySelector("div#builds").innerHTML = data.html
- gl.utils.localTimeAgo($('.js-timeago', 'div#builds'))
- @buildsLoaded = true
- @scrollToElement("#builds")
-
- # Show or hide the loading spinner
- #
- # status - Boolean, true to show, false to hide
- toggleLoading: (status) ->
- $('.mr-loading-status .loading').toggle(status)
-
- _get: (options) ->
- defaults = {
- beforeSend: => @toggleLoading(true)
- complete: => @toggleLoading(false)
- dataType: 'json'
- type: 'GET'
- }
-
- options = $.extend({}, defaults, options)
-
- $.ajax(options)
-
- # Returns diff view type
- diffViewType: ->
- $('.inline-parallel-buttons a.active').data('view-type')
-
- expandViewContainer: ->
- $('.container-fluid').removeClass('container-limited')
-
- shrinkView: ->
- $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: ->
- return if $.cookie('collapsed_gutter') == 'true'
-
- $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)
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
new file mode 100644
index 00000000000..bd35b6f679d
--- /dev/null
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -0,0 +1,185 @@
+(function() {
+ 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; };
+
+ this.MergeRequestWidget = (function() {
+ function MergeRequestWidget(opts) {
+ this.opts = opts;
+ $('#modal_merge_info').modal({
+ show: false
+ });
+ this.firstCICheck = true;
+ this.readyForCICheck = false;
+ this.cancel = false;
+ clearInterval(this.fetchBuildStatusInterval);
+ this.clearEventListeners();
+ this.addEventListeners();
+ this.getCIStatus(false);
+ this.pollCIStatus();
+ notifyPermissions();
+ }
+
+ MergeRequestWidget.prototype.clearEventListeners = function() {
+ 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) {
+ return function() {
+ var page;
+ page = $('body').data('page').split(':').last();
+ if (allowedPages.indexOf(page) < 0) {
+ clearInterval(_this.fetchBuildStatusInterval);
+ _this.cancelPolling();
+ return _this.clearEventListeners();
+ }
+ };
+ })(this));
+ };
+
+ MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
+ if (deleteSourceBranch == null) {
+ deleteSourceBranch = false;
+ }
+ return $.ajax({
+ type: 'GET',
+ url: $('.merge-request').data('url'),
+ success: (function(_this) {
+ return function(data) {
+ var callback, urlSuffix;
+ if (data.state === "merged") {
+ urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
+ return window.location.href = window.location.pathname + urlSuffix;
+ } else if (data.merge_error) {
+ return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
+ } else {
+ callback = function() {
+ return merge_request_widget.mergeInProgress(deleteSourceBranch);
+ };
+ return setTimeout(callback, 2000);
+ }
+ };
+ })(this),
+ dataType: 'json'
+ });
+ };
+
+ MergeRequestWidget.prototype.getMergeStatus = function() {
+ return $.get(this.opts.merge_check_url, function(data) {
+ return $('.mr-state-widget').replaceWith(data);
+ });
+ };
+
+ MergeRequestWidget.prototype.ciLabelForStatus = function(status) {
+ switch (status) {
+ case 'success':
+ return 'passed';
+ case 'success_with_warnings':
+ return 'passed with warnings';
+ default:
+ return status;
+ }
+ };
+
+ 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;
+ $('.ci-widget-fetching').show();
+ 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 (_this.firstCICheck || 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);
+ }
+ if (showNotification && !_this.firstCICheck) {
+ status = _this.ciLabelForStatus(data.status);
+ if (status === "preparing") {
+ title = _this.opts.ci_title.preparing;
+ status = status.charAt(0).toUpperCase() + status.slice(1);
+ message = _this.opts.ci_message.preparing.replace('{{status}}', status);
+ } else {
+ title = _this.opts.ci_title.normal;
+ message = _this.opts.ci_message.normal.replace('{{status}}', status);
+ }
+ title = title.replace('{{status}}', status);
+ message = message.replace('{{sha}}', data.sha);
+ message = message.replace('{{title}}', data.title);
+ notify(title, message, _this.opts.gitlab_icon, function() {
+ this.close();
+ return Turbolinks.visit(_this.opts.builds_path);
+ });
+ }
+ return _this.firstCICheck = false;
+ }
+ };
+ })(this));
+ };
+
+ MergeRequestWidget.prototype.showCIStatus = function(state) {
+ var allowed_states;
+ if (state == null) {
+ return;
+ }
+ $('.ci_widget').hide();
+ allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"];
+ if (indexOf.call(allowed_states, state) >= 0) {
+ $('.ci_widget.ci-' + state).show();
+ switch (state) {
+ case "failed":
+ case "canceled":
+ case "not_found":
+ return this.setMergeButtonClass('btn-danger');
+ case "running":
+ return this.setMergeButtonClass('btn-warning');
+ case "success":
+ case "success_with_warnings":
+ return this.setMergeButtonClass('btn-create');
+ }
+ } else {
+ $('.ci_widget.ci-error').show();
+ return this.setMergeButtonClass('btn-danger');
+ }
+ };
+
+ MergeRequestWidget.prototype.showCICoverage = function(coverage) {
+ var text;
+ text = 'Coverage ' + coverage + '%';
+ return $('.ci_widget:visible .ci-coverage').text(text);
+ };
+
+ 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 MergeRequestWidget;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
deleted file mode 100644
index 779f536d9f0..00000000000
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ /dev/null
@@ -1,140 +0,0 @@
-class @MergeRequestWidget
- # Initialize MergeRequestWidget behavior
- #
- # check_enable - Boolean, whether to check automerge status
- # merge_check_url - String, URL to use to check automerge status
- # ci_status_url - String, URL to use to check CI status
- #
-
- constructor: (@opts) ->
- $('#modal_merge_info').modal(show: false)
- @firstCICheck = true
- @readyForCICheck = false
- @cancel = false
- clearInterval @fetchBuildStatusInterval
-
- @clearEventListeners()
- @addEventListeners()
- @getCIStatus(false)
- @pollCIStatus()
- notifyPermissions()
-
- clearEventListeners: ->
- $(document).off 'page:change.merge_request'
-
- cancelPolling: ->
- @cancel = true
-
- addEventListeners: ->
- allowedPages = ['show', 'commits', 'builds', 'changes']
- $(document).on 'page:change.merge_request', =>
- page = $('body').data('page').split(':').last()
- if allowedPages.indexOf(page) < 0
- clearInterval @fetchBuildStatusInterval
- @cancelPolling()
- @clearEventListeners()
-
- mergeInProgress: (deleteSourceBranch = false)->
- $.ajax
- type: 'GET'
- url: $('.merge-request').data('url')
- success: (data) =>
- if data.state == "merged"
- urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
-
- window.location.href = window.location.pathname + urlSuffix
- else if data.merge_error
- $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
- else
- callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch)
- setTimeout(callback, 2000)
- dataType: 'json'
-
- getMergeStatus: ->
- $.get @opts.merge_check_url, (data) ->
- $('.mr-state-widget').replaceWith(data)
-
- ciLabelForStatus: (status) ->
- if status is 'success'
- 'passed'
- else
- status
-
- pollCIStatus: ->
- @fetchBuildStatusInterval = setInterval ( =>
- return if not @readyForCICheck
-
- @getCIStatus(true)
-
- @readyForCICheck = false
- ), 10000
-
- getCIStatus: (showNotification) ->
- _this = @
- $('.ci-widget-fetching').show()
-
- $.getJSON @opts.ci_status_url, (data) =>
- return if @cancel
- @readyForCICheck = true
-
- if data.status is ''
- return
-
- if @firstCICheck || data.status isnt @opts.ci_status and data.status?
- @opts.ci_status = data.status
- @showCIStatus data.status
- if data.coverage
- @showCICoverage data.coverage
-
- # The first check should only update the UI, a notification
- # should only be displayed on status changes
- if showNotification and not @firstCICheck
- status = @ciLabelForStatus(data.status)
-
- if status is "preparing"
- title = @opts.ci_title.preparing
- status = status.charAt(0).toUpperCase() + status.slice(1);
- message = @opts.ci_message.preparing.replace('{{status}}', status)
- else
- title = @opts.ci_title.normal
- message = @opts.ci_message.normal.replace('{{status}}', status)
-
- title = title.replace('{{status}}', status)
- message = message.replace('{{sha}}', data.sha)
- message = message.replace('{{title}}', data.title)
-
- notify(
- title,
- message,
- @opts.gitlab_icon,
- ->
- @close()
- Turbolinks.visit _this.opts.builds_path
- )
- @firstCICheck = false
-
- showCIStatus: (state) ->
- return if not state?
- $('.ci_widget').hide()
- allowed_states = ["failed", "canceled", "running", "pending", "success", "skipped", "not_found"]
- if state in allowed_states
- $('.ci_widget.ci-' + state).show()
- switch state
- when "failed", "canceled", "not_found"
- @setMergeButtonClass('btn-danger')
- when "running"
- @setMergeButtonClass('btn-warning')
- when "success"
- @setMergeButtonClass('btn-create')
- else
- $('.ci_widget.ci-error').show()
- @setMergeButtonClass('btn-danger')
-
- showCICoverage: (coverage) ->
- text = 'Coverage ' + coverage + '%'
- $('.ci_widget:visible .ci-coverage').text(text)
-
- setMergeButtonClass: (css_class) ->
- $('.js-merge-button,.accept-action .dropdown-toggle')
- .removeClass('btn-danger btn-warning btn-create')
- .addClass(css_class)
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
new file mode 100644
index 00000000000..1fed38661a2
--- /dev/null
+++ b/app/assets/javascripts/merged_buttons.js
@@ -0,0 +1,45 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.MergedButtons = (function() {
+ function MergedButtons() {
+ this.removeSourceBranch = bind(this.removeSourceBranch, this);
+ this.$removeBranchWidget = $('.remove_source_branch_widget');
+ this.$removeBranchProgress = $('.remove_source_branch_in_progress');
+ this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
+ this.cleanEventListeners();
+ this.initEventListeners();
+ }
+
+ MergedButtons.prototype.cleanEventListeners = function() {
+ $(document).off('click', '.remove_source_branch');
+ $(document).off('ajax:success', '.remove_source_branch');
+ return $(document).off('ajax:error', '.remove_source_branch');
+ };
+
+ MergedButtons.prototype.initEventListeners = function() {
+ $(document).on('click', '.remove_source_branch', this.removeSourceBranch);
+ $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
+ return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
+ };
+
+ MergedButtons.prototype.removeSourceBranch = function() {
+ this.$removeBranchWidget.hide();
+ return this.$removeBranchProgress.show();
+ };
+
+ MergedButtons.prototype.removeBranchSuccess = function() {
+ return location.reload();
+ };
+
+ MergedButtons.prototype.removeBranchError = function() {
+ this.$removeBranchWidget.hide();
+ this.$removeBranchProgress.hide();
+ return this.$removeBranchFailed.show();
+ };
+
+ return MergedButtons;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/merged_buttons.js.coffee b/app/assets/javascripts/merged_buttons.js.coffee
deleted file mode 100644
index 4929295c10b..00000000000
--- a/app/assets/javascripts/merged_buttons.js.coffee
+++ /dev/null
@@ -1,30 +0,0 @@
-class @MergedButtons
- constructor: ->
- @$removeBranchWidget = $('.remove_source_branch_widget')
- @$removeBranchProgress = $('.remove_source_branch_in_progress')
- @$removeBranchFailed = $('.remove_source_branch_widget.failed')
-
- @cleanEventListeners()
- @initEventListeners()
-
- cleanEventListeners: ->
- $(document).off 'click', '.remove_source_branch'
- $(document).off 'ajax:success', '.remove_source_branch'
- $(document).off 'ajax:error', '.remove_source_branch'
-
- initEventListeners: ->
- $(document).on 'click', '.remove_source_branch', @removeSourceBranch
- $(document).on 'ajax:success', '.remove_source_branch', @removeBranchSuccess
- $(document).on 'ajax:error', '.remove_source_branch', @removeBranchError
-
- removeSourceBranch: =>
- @$removeBranchWidget.hide()
- @$removeBranchProgress.show()
-
- removeBranchSuccess: ->
- location.reload()
-
- removeBranchError: ->
- @$removeBranchWidget.hide()
- @$removeBranchProgress.hide()
- @$removeBranchFailed.show()
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
new file mode 100644
index 00000000000..e8d51da7d58
--- /dev/null
+++ b/app/assets/javascripts/milestone.js
@@ -0,0 +1,195 @@
+(function() {
+ this.Milestone = (function() {
+ Milestone.updateIssue = function(li, issue_url, data) {
+ return $.ajax({
+ type: "PUT",
+ url: issue_url,
+ data: data,
+ success: (function(_this) {
+ return function(_data) {
+ return _this.successCallback(_data, li);
+ };
+ })(this),
+ error: function(data) {
+ return new Flash("Issue update failed", 'alert');
+ },
+ dataType: "json"
+ });
+ };
+
+ Milestone.sortIssues = function(data) {
+ var sort_issues_url;
+ sort_issues_url = location.href + "/sort_issues";
+ return $.ajax({
+ type: "PUT",
+ url: sort_issues_url,
+ data: data,
+ success: (function(_this) {
+ return function(_data) {
+ return _this.successCallback(_data);
+ };
+ })(this),
+ error: function() {
+ return new Flash("Issues update failed", 'alert');
+ },
+ dataType: "json"
+ });
+ };
+
+ Milestone.sortMergeRequests = function(data) {
+ var sort_mr_url;
+ sort_mr_url = location.href + "/sort_merge_requests";
+ return $.ajax({
+ type: "PUT",
+ url: sort_mr_url,
+ data: data,
+ success: (function(_this) {
+ return function(_data) {
+ return _this.successCallback(_data);
+ };
+ })(this),
+ error: function(data) {
+ return new Flash("Issue update failed", 'alert');
+ },
+ dataType: "json"
+ });
+ };
+
+ Milestone.updateMergeRequest = function(li, merge_request_url, data) {
+ return $.ajax({
+ type: "PUT",
+ url: merge_request_url,
+ data: data,
+ success: (function(_this) {
+ return function(_data) {
+ return _this.successCallback(_data, li);
+ };
+ })(this),
+ error: function(data) {
+ return new Flash("Issue update failed", 'alert');
+ },
+ dataType: "json"
+ });
+ };
+
+ Milestone.successCallback = function(data, element) {
+ var img_tag;
+ if (data.assignee) {
+ img_tag = $('<img/>');
+ img_tag.attr('src', data.assignee.avatar_url);
+ img_tag.addClass('avatar s16');
+ $(element).find('.assignee-icon').html(img_tag);
+ } else {
+ $(element).find('.assignee-icon').html('');
+ }
+ return $(element).effect('highlight');
+ };
+
+ function Milestone() {
+ var oldMouseStart;
+ oldMouseStart = $.ui.sortable.prototype._mouseStart;
+ $.ui.sortable.prototype._mouseStart = function(event, overrideHandle, noActivation) {
+ this._trigger("beforeStart", event, this._uiHash());
+ return oldMouseStart.apply(this, [event, overrideHandle, noActivation]);
+ };
+ this.bindIssuesSorting();
+ this.bindMergeRequestSorting();
+ this.bindTabsSwitching();
+ }
+
+ Milestone.prototype.bindIssuesSorting = function() {
+ return $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable({
+ connectWith: ".issues-sortable-list",
+ dropOnEmpty: true,
+ items: "li:not(.ui-sort-disabled)",
+ beforeStart: function(event, ui) {
+ return $(".issues-sortable-list").css("min-height", ui.item.outerHeight());
+ },
+ stop: function(event, ui) {
+ return $(".issues-sortable-list").css("min-height", "0px");
+ },
+ update: function(event, ui) {
+ var data;
+ if ($(this).find(ui.item).length > 0) {
+ data = $(this).sortable("serialize");
+ return Milestone.sortIssues(data);
+ }
+ },
+ receive: function(event, ui) {
+ var data, issue_id, issue_url, new_state;
+ new_state = $(this).data('state');
+ issue_id = ui.item.data('iid');
+ issue_url = ui.item.data('url');
+ data = (function() {
+ switch (new_state) {
+ case 'ongoing':
+ return "issue[assignee_id]=" + gon.current_user_id;
+ case 'unassigned':
+ return "issue[assignee_id]=";
+ case 'closed':
+ return "issue[state_event]=close";
+ }
+ })();
+ if ($(ui.sender).data('state') === "closed") {
+ data += "&issue[state_event]=reopen";
+ }
+ return Milestone.updateIssue(ui.item, issue_url, data);
+ }
+ }).disableSelection();
+ };
+
+ Milestone.prototype.bindTabsSwitching = function() {
+ return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) {
+ var currentTabClass, previousTabClass;
+ currentTabClass = $(e.target).data('show');
+ previousTabClass = $(e.relatedTarget).data('show');
+ $(previousTabClass).hide();
+ $(currentTabClass).removeClass('hidden');
+ return $(currentTabClass).show();
+ });
+ };
+
+ Milestone.prototype.bindMergeRequestSorting = function() {
+ return $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable({
+ connectWith: ".merge_requests-sortable-list",
+ dropOnEmpty: true,
+ items: "li:not(.ui-sort-disabled)",
+ beforeStart: function(event, ui) {
+ return $(".merge_requests-sortable-list").css("min-height", ui.item.outerHeight());
+ },
+ stop: function(event, ui) {
+ return $(".merge_requests-sortable-list").css("min-height", "0px");
+ },
+ update: function(event, ui) {
+ var data;
+ data = $(this).sortable("serialize");
+ return Milestone.sortMergeRequests(data);
+ },
+ receive: function(event, ui) {
+ var data, merge_request_id, merge_request_url, new_state;
+ new_state = $(this).data('state');
+ merge_request_id = ui.item.data('iid');
+ merge_request_url = ui.item.data('url');
+ data = (function() {
+ switch (new_state) {
+ case 'ongoing':
+ return "merge_request[assignee_id]=" + gon.current_user_id;
+ case 'unassigned':
+ return "merge_request[assignee_id]=";
+ case 'closed':
+ return "merge_request[state_event]=close";
+ }
+ })();
+ if ($(ui.sender).data('state') === "closed") {
+ data += "&merge_request[state_event]=reopen";
+ }
+ return Milestone.updateMergeRequest(ui.item, merge_request_url, data);
+ }
+ }).disableSelection();
+ };
+
+ return Milestone;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/milestone.js.coffee b/app/assets/javascripts/milestone.js.coffee
deleted file mode 100644
index a19e68b39e2..00000000000
--- a/app/assets/javascripts/milestone.js.coffee
+++ /dev/null
@@ -1,146 +0,0 @@
-class @Milestone
- @updateIssue: (li, issue_url, data) ->
- $.ajax
- type: "PUT"
- url: issue_url
- data: data
- success: (_data) =>
- @successCallback(_data, li)
- error: (data) ->
- new Flash("Issue update failed", 'alert')
- dataType: "json"
-
- @sortIssues: (data) ->
- sort_issues_url = location.href + "/sort_issues"
-
- $.ajax
- type: "PUT"
- url: sort_issues_url
- data: data
- success: (_data) =>
- @successCallback(_data)
- error: ->
- new Flash("Issues update failed", 'alert')
- dataType: "json"
-
- @sortMergeRequests: (data) ->
- sort_mr_url = location.href + "/sort_merge_requests"
-
- $.ajax
- type: "PUT"
- url: sort_mr_url
- data: data
- success: (_data) =>
- @successCallback(_data)
- error: (data) ->
- new Flash("Issue update failed", 'alert')
- dataType: "json"
-
- @updateMergeRequest: (li, merge_request_url, data) ->
- $.ajax
- type: "PUT"
- url: merge_request_url
- data: data
- success: (_data) =>
- @successCallback(_data, li)
- error: (data) ->
- new Flash("Issue update failed", 'alert')
- dataType: "json"
-
- @successCallback: (data, element) =>
- if data.assignee
- img_tag = $('<img/>')
- img_tag.attr('src', data.assignee.avatar_url)
- img_tag.addClass('avatar s16')
- $(element).find('.assignee-icon').html(img_tag)
- else
- $(element).find('.assignee-icon').html('')
-
- $(element).effect 'highlight'
-
- constructor: ->
- oldMouseStart = $.ui.sortable.prototype._mouseStart
- $.ui.sortable.prototype._mouseStart = (event, overrideHandle, noActivation) ->
- this._trigger "beforeStart", event, this._uiHash()
- oldMouseStart.apply this, [event, overrideHandle, noActivation]
-
- @bindIssuesSorting()
- @bindMergeRequestSorting()
- @bindTabsSwitching()
-
- bindIssuesSorting: ->
- $("#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed").sortable(
- connectWith: ".issues-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: (event, ui) ->
- $(".issues-sortable-list").css "min-height", ui.item.outerHeight()
- stop: (event, ui) ->
- $(".issues-sortable-list").css "min-height", "0px"
- update: (event, ui) ->
- # Prevents sorting from container which element has been removed.
- if $(this).find(ui.item).length > 0
- data = $(this).sortable("serialize")
- Milestone.sortIssues(data)
-
- receive: (event, ui) ->
- new_state = $(this).data('state')
- issue_id = ui.item.data('iid')
- issue_url = ui.item.data('url')
-
- data = switch new_state
- when 'ongoing'
- "issue[assignee_id]=" + gon.current_user_id
- when 'unassigned'
- "issue[assignee_id]="
- when 'closed'
- "issue[state_event]=close"
-
- if $(ui.sender).data('state') == "closed"
- data += "&issue[state_event]=reopen"
-
- Milestone.updateIssue(ui.item, issue_url, data)
-
- ).disableSelection()
-
- bindTabsSwitching: ->
- $('a[data-toggle="tab"]').on 'show.bs.tab', (e) ->
- currentTabClass = $(e.target).data('show')
- previousTabClass = $(e.relatedTarget).data('show')
-
- $(previousTabClass).hide()
- $(currentTabClass).removeClass('hidden')
- $(currentTabClass).show()
-
- bindMergeRequestSorting: ->
- $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").sortable(
- connectWith: ".merge_requests-sortable-list",
- dropOnEmpty: true,
- items: "li:not(.ui-sort-disabled)",
- beforeStart: (event, ui) ->
- $(".merge_requests-sortable-list").css "min-height", ui.item.outerHeight()
- stop: (event, ui) ->
- $(".merge_requests-sortable-list").css "min-height", "0px"
- update: (event, ui) ->
- data = $(this).sortable("serialize")
- Milestone.sortMergeRequests(data)
-
- receive: (event, ui) ->
- new_state = $(this).data('state')
- merge_request_id = ui.item.data('iid')
- merge_request_url = ui.item.data('url')
-
- data = switch new_state
- when 'ongoing'
- "merge_request[assignee_id]=" + gon.current_user_id
- when 'unassigned'
- "merge_request[assignee_id]="
- when 'closed'
- "merge_request[state_event]=close"
-
- if $(ui.sender).data('state') == "closed"
- data += "&merge_request[state_event]=reopen"
-
- Milestone.updateMergeRequest(ui.item, merge_request_url, data)
-
- ).disableSelection()
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
new file mode 100644
index 00000000000..e897ebdf630
--- /dev/null
+++ b/app/assets/javascripts/milestone_select.js
@@ -0,0 +1,155 @@
+(function() {
+ this.MilestoneSelect = (function() {
+ function MilestoneSelect(currentProject) {
+ var _this;
+ if (currentProject != null) {
+ _this = this;
+ this.currentProject = JSON.parse(currentProject);
+ }
+ $('.js-milestone-select').each(function(i, dropdown) {
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId;
+ $dropdown = $(dropdown);
+ projectId = $dropdown.data('project-id');
+ milestonesUrl = $dropdown.data('milestones');
+ issueUpdateURL = $dropdown.data('issueUpdate');
+ selectedMilestone = $dropdown.data('selected');
+ showNo = $dropdown.data('show-no');
+ showAny = $dropdown.data('show-any');
+ showUpcoming = $dropdown.data('show-upcoming');
+ useId = $dropdown.data('use-id');
+ defaultLabel = $dropdown.data('default-label');
+ issuableId = $dropdown.data('issuable-id');
+ abilityName = $dropdown.data('ability-name');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon');
+ $value = $block.find('.value');
+ $loading = $block.find('.block-loading').fadeOut();
+ if (issueUpdateURL) {
+ milestoneLinkTemplate = _.template('<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>');
+ milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
+ collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
+ }
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: milestonesUrl
+ }).done(function(data) {
+ var extraOptions;
+ extraOptions = [];
+ if (showAny) {
+ extraOptions.push({
+ id: 0,
+ name: '',
+ title: 'Any Milestone'
+ });
+ }
+ if (showNo) {
+ extraOptions.push({
+ id: -1,
+ name: 'No Milestone',
+ title: 'No Milestone'
+ });
+ }
+ if (showUpcoming) {
+ extraOptions.push({
+ id: -2,
+ name: '#upcoming',
+ title: 'Upcoming'
+ });
+ }
+ if (extraOptions.length > 2) {
+ extraOptions.push('divider');
+ }
+ return callback(extraOptions.concat(data));
+ });
+ },
+ filterable: true,
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ toggleLabel: function(selected) {
+ if (selected && 'id' in selected) {
+ return selected.title;
+ } else {
+ return defaultLabel;
+ }
+ },
+ fieldName: $dropdown.data('field-name'),
+ text: function(milestone) {
+ return _.escape(milestone.title);
+ },
+ id: function(milestone) {
+ if (!useId) {
+ return milestone.name;
+ } else {
+ return milestone.id;
+ }
+ },
+ isSelected: function(milestone) {
+ return milestone.name === selectedMilestone;
+ },
+ hidden: function() {
+ $selectbox.hide();
+ return $value.css('display', '');
+ },
+ clicked: function(selected, $el, e) {
+ var data, isIssueIndex, isMRIndex, page;
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ return;
+ }
+ if (page === 'projects:boards:show') {
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ if (selected.name != null) {
+ selectedMilestone = selected.name;
+ } else {
+ selectedMilestone = '';
+ }
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else {
+ selected = $selectbox.find('input[type="hidden"]').val();
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].milestone_id = selected != null ? selected : null;
+ $loading.fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ url: issueUpdateURL,
+ data: data
+ }).done(function(data) {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ $selectbox.hide();
+ $value.css('display', '');
+ if (data.milestone != null) {
+ data.milestone.namespace = _this.currentProject.namespace;
+ data.milestone.path = _this.currentProject.path;
+ data.milestone.remaining = $.timefor(data.milestone.due_date);
+ $value.html(milestoneLinkTemplate(data.milestone));
+ return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
+ } else {
+ $value.html(milestoneLinkNoneTemplate);
+ return $sidebarCollapsedValue.find('span').text('No');
+ }
+ });
+ }
+ }
+ });
+ });
+ }
+
+ return MilestoneSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
deleted file mode 100644
index 8ab03ed93ee..00000000000
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ /dev/null
@@ -1,137 +0,0 @@
-class @MilestoneSelect
- constructor: (currentProject) ->
- if currentProject?
- _this = @
- @currentProject = JSON.parse(currentProject)
- $('.js-milestone-select').each (i, dropdown) ->
- $dropdown = $(dropdown)
- projectId = $dropdown.data('project-id')
- milestonesUrl = $dropdown.data('milestones')
- issueUpdateURL = $dropdown.data('issueUpdate')
- selectedMilestone = $dropdown.data('selected')
- showNo = $dropdown.data('show-no')
- showAny = $dropdown.data('show-any')
- showUpcoming = $dropdown.data('show-upcoming')
- useId = $dropdown.data('use-id')
- defaultLabel = $dropdown.data('default-label')
- issuableId = $dropdown.data('issuable-id')
- abilityName = $dropdown.data('ability-name')
- $selectbox = $dropdown.closest('.selectbox')
- $block = $selectbox.closest('.block')
- $sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon')
- $value = $block.find('.value')
- $loading = $block.find('.block-loading').fadeOut()
-
- if issueUpdateURL
- milestoneLinkTemplate = _.template(
- '<a href="/<%- namespace %>/<%- path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'
- )
-
- milestoneLinkNoneTemplate = '<span class="no-value">None</span>'
-
- collapsedSidebarLabelTemplate = _.template(
- '<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left">
- <%- title %>
- </span>'
- )
-
- $dropdown.glDropdown(
- data: (term, callback) ->
- $.ajax(
- url: milestonesUrl
- ).done (data) ->
- extraOptions = []
- if showAny
- extraOptions.push(
- id: 0
- name: ''
- title: 'Any Milestone'
- )
-
- if showNo
- extraOptions.push(
- id: -1
- name: 'No Milestone'
- title: 'No Milestone'
- )
-
- if showUpcoming
- extraOptions.push(
- id: -2
- name: '#upcoming'
- title: 'Upcoming'
- )
-
- if extraOptions.length > 2
- extraOptions.push 'divider'
-
- callback(extraOptions.concat(data))
- filterable: true
- search:
- fields: ['title']
- selectable: true
- toggleLabel: (selected) ->
- if selected && 'id' of selected
- selected.title
- else
- defaultLabel
- fieldName: $dropdown.data('field-name')
- text: (milestone) ->
- _.escape(milestone.title)
- id: (milestone) ->
- if !useId
- milestone.name
- else
- milestone.id
- isSelected: (milestone) ->
- milestone.name is selectedMilestone
- hidden: ->
- $selectbox.hide()
-
- # display:block overrides the hide-collapse rule
- $value.css('display', '')
- clicked: (selected) ->
- page = $('body').data 'page'
- isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is page is 'projects:merge_requests:index'
-
- if $dropdown.hasClass 'js-filter-bulk-update'
- return
-
- if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- if selected.name?
- selectedMilestone = selected.name
- else
- selectedMilestone = ''
- Issuable.filterResults $dropdown.closest('form')
- else if $dropdown.hasClass('js-filter-submit')
- $dropdown.closest('form').submit()
- else
- selected = $selectbox
- .find('input[type="hidden"]')
- .val()
- data = {}
- data[abilityName] = {}
- data[abilityName].milestone_id = if selected? then selected else null
- $loading
- .fadeIn()
- $dropdown.trigger('loading.gl.dropdown')
- $.ajax(
- type: 'PUT'
- url: issueUpdateURL
- data: data
- ).done (data) ->
- $dropdown.trigger('loaded.gl.dropdown')
- $loading.fadeOut()
- $selectbox.hide()
- $value.css('display', '')
- if data.milestone?
- data.milestone.namespace = _this.currentProject.namespace
- data.milestone.path = _this.currentProject.path
- data.milestone.remaining = $.timefor data.milestone.due_date
- $value.html(milestoneLinkTemplate(data.milestone))
- $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone))
- else
- $value.html(milestoneLinkNoneTemplate)
- $sidebarCollapsedValue.find('span').text('No')
- )
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
new file mode 100644
index 00000000000..10f4fd106d8
--- /dev/null
+++ b/app/assets/javascripts/namespace_select.js
@@ -0,0 +1,86 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.NamespaceSelect = (function() {
+ function NamespaceSelect(opts) {
+ this.onSelectItem = bind(this.onSelectItem, this);
+ var fieldName, showAny;
+ this.dropdown = opts.dropdown;
+ showAny = true;
+ fieldName = 'namespace_id';
+ if (this.dropdown.attr('data-field-name')) {
+ fieldName = this.dropdown.data('fieldName');
+ }
+ if (this.dropdown.attr('data-show-any')) {
+ showAny = this.dropdown.data('showAny');
+ }
+ this.dropdown.glDropdown({
+ filterable: true,
+ selectable: true,
+ filterRemote: true,
+ search: {
+ fields: ['path']
+ },
+ fieldName: fieldName,
+ toggleLabel: function(selected) {
+ if (selected.id == null) {
+ return selected.text;
+ } else {
+ return selected.kind + ": " + selected.path;
+ }
+ },
+ data: function(term, dataCallback) {
+ return Api.namespaces(term, function(namespaces) {
+ var anyNamespace;
+ if (showAny) {
+ anyNamespace = {
+ text: 'Any namespace',
+ id: null
+ };
+ namespaces.unshift(anyNamespace);
+ namespaces.splice(1, 0, 'divider');
+ }
+ return dataCallback(namespaces);
+ });
+ },
+ text: function(namespace) {
+ if (namespace.id == null) {
+ return namespace.text;
+ } else {
+ return namespace.kind + ": " + namespace.path;
+ }
+ },
+ renderRow: this.renderRow,
+ clicked: this.onSelectItem
+ });
+ }
+
+ NamespaceSelect.prototype.onSelectItem = function(item, el, e) {
+ return e.preventDefault();
+ };
+
+ return NamespaceSelect;
+
+ })();
+
+ this.NamespaceSelects = (function() {
+ function NamespaceSelects(opts) {
+ var ref;
+ if (opts == null) {
+ opts = {};
+ }
+ this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-namespace-select');
+ this.$dropdowns.each(function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return new NamespaceSelect({
+ dropdown: $dropdown
+ });
+ });
+ }
+
+ return NamespaceSelects;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/namespace_select.js.coffee b/app/assets/javascripts/namespace_select.js.coffee
deleted file mode 100644
index 3b419dff105..00000000000
--- a/app/assets/javascripts/namespace_select.js.coffee
+++ /dev/null
@@ -1,56 +0,0 @@
-class @NamespaceSelect
- constructor: (opts) ->
- {
- @dropdown
- } = opts
-
- showAny = true
- fieldName = 'namespace_id'
-
- if @dropdown.attr 'data-field-name'
- fieldName = @dropdown.data 'fieldName'
-
- if @dropdown.attr 'data-show-any'
- showAny = @dropdown.data 'showAny'
-
- @dropdown.glDropdown(
- filterable: true
- selectable: true
- filterRemote: true
- search:
- fields: ['path']
- fieldName: fieldName
- toggleLabel: (selected) ->
- return if not selected.id? then selected.text else "#{selected.kind}: #{selected.path}"
- data: (term, dataCallback) ->
- Api.namespaces term, (namespaces) ->
- if showAny
- anyNamespace =
- text: 'Any namespace'
- id: null
-
- namespaces.unshift(anyNamespace)
- namespaces.splice 1, 0, 'divider'
-
- dataCallback(namespaces)
- text: (namespace) ->
- return if not namespace.id? then namespace.text else "#{namespace.kind}: #{namespace.path}"
- renderRow: @renderRow
- clicked: @onSelectItem
- )
-
- onSelectItem: (item, el, e) =>
- e.preventDefault()
-
-class @NamespaceSelects
- constructor: (opts = {}) ->
- {
- @$dropdowns = $('.js-namespace-select')
- } = opts
-
- @$dropdowns.each (i, dropdown) ->
- $dropdown = $(dropdown)
-
- new NamespaceSelect(
- dropdown: $dropdown
- )
diff --git a/app/assets/javascripts/network/application.js.coffee b/app/assets/javascripts/network/application.js.coffee
deleted file mode 100644
index f75f63869c5..00000000000
--- a/app/assets/javascripts/network/application.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-# This is a manifest file that'll be compiled into including all the files listed below.
-# Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
-# be included in the compiled file accessible from http://example.com/assets/application.js
-# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-# the compiled file.
-#
-#= require_tree .
-
-$ ->
- network_graph = new Network({
- url: $(".network-graph").attr('data-url'),
- commit_url: $(".network-graph").attr('data-commit-url'),
- ref: $(".network-graph").attr('data-ref'),
- commit_id: $(".network-graph").attr('data-commit-id')
- })
-
- new ShortcutsNetwork(network_graph.branch_graph)
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js
new file mode 100644
index 00000000000..c0fec1f8607
--- /dev/null
+++ b/app/assets/javascripts/network/branch-graph.js
@@ -0,0 +1,404 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.BranchGraph = (function() {
+ function BranchGraph(element1, options1) {
+ this.element = element1;
+ this.options = options1;
+ this.scrollTop = bind(this.scrollTop, this);
+ this.scrollBottom = bind(this.scrollBottom, this);
+ this.scrollRight = bind(this.scrollRight, this);
+ this.scrollLeft = bind(this.scrollLeft, this);
+ this.scrollUp = bind(this.scrollUp, this);
+ this.scrollDown = bind(this.scrollDown, this);
+ this.preparedCommits = {};
+ this.mtime = 0;
+ this.mspace = 0;
+ this.parents = {};
+ this.colors = ["#000"];
+ this.offsetX = 150;
+ this.offsetY = 20;
+ this.unitTime = 30;
+ this.unitSpace = 10;
+ this.prev_start = -1;
+ this.load();
+ }
+
+ BranchGraph.prototype.load = function() {
+ return $.ajax({
+ url: this.options.url,
+ method: "get",
+ dataType: "json",
+ success: $.proxy(function(data) {
+ $(".loading", this.element).hide();
+ this.prepareData(data.days, data.commits);
+ return this.buildGraph();
+ }, this)
+ });
+ };
+
+ BranchGraph.prototype.prepareData = function(days, commits) {
+ var c, ch, cw, j, len, ref;
+ this.days = days;
+ this.commits = commits;
+ this.collectParents();
+ this.graphHeight = $(this.element).height();
+ this.graphWidth = $(this.element).width();
+ ch = Math.max(this.graphHeight, this.offsetY + this.unitTime * this.mtime + 150);
+ cw = Math.max(this.graphWidth, this.offsetX + this.unitSpace * this.mspace + 300);
+ this.r = Raphael(this.element.get(0), cw, ch);
+ this.top = this.r.set();
+ this.barHeight = Math.max(this.graphHeight, this.unitTime * this.days.length + 320);
+ ref = this.commits;
+ for (j = 0, len = ref.length; j < len; j++) {
+ c = ref[j];
+ if (c.id in this.parents) {
+ c.isParent = true;
+ }
+ this.preparedCommits[c.id] = c;
+ this.markCommit(c);
+ }
+ return this.collectColors();
+ };
+
+ BranchGraph.prototype.collectParents = function() {
+ var c, j, len, p, ref, results;
+ ref = this.commits;
+ results = [];
+ for (j = 0, len = ref.length; j < len; j++) {
+ c = ref[j];
+ this.mtime = Math.max(this.mtime, c.time);
+ this.mspace = Math.max(this.mspace, c.space);
+ results.push((function() {
+ var l, len1, ref1, results1;
+ ref1 = c.parents;
+ results1 = [];
+ for (l = 0, len1 = ref1.length; l < len1; l++) {
+ p = ref1[l];
+ this.parents[p[0]] = true;
+ results1.push(this.mspace = Math.max(this.mspace, p[1]));
+ }
+ return results1;
+ }).call(this));
+ }
+ return results;
+ };
+
+ BranchGraph.prototype.collectColors = function() {
+ var k, results;
+ k = 0;
+ results = [];
+ while (k < this.mspace) {
+ this.colors.push(Raphael.getColor(.8));
+ Raphael.getColor();
+ Raphael.getColor();
+ results.push(k++);
+ }
+ return results;
+ };
+
+ BranchGraph.prototype.buildGraph = function() {
+ var cuday, cumonth, day, j, len, mm, r, ref;
+ r = this.r;
+ cuday = 0;
+ cumonth = "";
+ r.rect(0, 0, 40, this.barHeight).attr({
+ fill: "#222"
+ });
+ r.rect(40, 0, 30, this.barHeight).attr({
+ fill: "#444"
+ });
+ ref = this.days;
+ for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
+ day = ref[mm];
+ if (cuday !== day[0] || cumonth !== day[1]) {
+ r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
+ font: "12px Monaco, monospace",
+ fill: "#BBB"
+ });
+ cuday = day[0];
+ }
+ if (cumonth !== day[1]) {
+ r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
+ font: "12px Monaco, monospace",
+ fill: "#EEE"
+ });
+ cumonth = day[1];
+ }
+ }
+ this.renderPartialGraph();
+ return this.bindEvents();
+ };
+
+ BranchGraph.prototype.renderPartialGraph = function() {
+ var commit, end, i, isGraphEdge, start, x, y;
+ start = Math.floor((this.element.scrollTop() - this.offsetY) / this.unitTime) - 10;
+ if (start < 0) {
+ isGraphEdge = true;
+ start = 0;
+ }
+ end = start + 40;
+ if (this.commits.length < end) {
+ isGraphEdge = true;
+ end = this.commits.length;
+ }
+ if (this.prev_start === -1 || Math.abs(this.prev_start - start) > 10 || isGraphEdge) {
+ i = start;
+ this.prev_start = start;
+ while (i < end) {
+ commit = this.commits[i];
+ i += 1;
+ if (commit.hasDrawn !== true) {
+ x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ y = this.offsetY + this.unitTime * commit.time;
+ this.drawDot(x, y, commit);
+ this.drawLines(x, y, commit);
+ this.appendLabel(x, y, commit);
+ this.appendAnchor(x, y, commit);
+ commit.hasDrawn = true;
+ }
+ }
+ return this.top.toFront();
+ }
+ };
+
+ BranchGraph.prototype.bindEvents = function() {
+ var element;
+ element = this.element;
+ return $(element).scroll((function(_this) {
+ return function(event) {
+ return _this.renderPartialGraph();
+ };
+ })(this));
+ };
+
+ BranchGraph.prototype.scrollDown = function() {
+ this.element.scrollTop(this.element.scrollTop() + 50);
+ return this.renderPartialGraph();
+ };
+
+ BranchGraph.prototype.scrollUp = function() {
+ this.element.scrollTop(this.element.scrollTop() - 50);
+ return this.renderPartialGraph();
+ };
+
+ BranchGraph.prototype.scrollLeft = function() {
+ this.element.scrollLeft(this.element.scrollLeft() - 50);
+ return this.renderPartialGraph();
+ };
+
+ BranchGraph.prototype.scrollRight = function() {
+ this.element.scrollLeft(this.element.scrollLeft() + 50);
+ return this.renderPartialGraph();
+ };
+
+ BranchGraph.prototype.scrollBottom = function() {
+ return this.element.scrollTop(this.element.find('svg').height());
+ };
+
+ BranchGraph.prototype.scrollTop = function() {
+ return this.element.scrollTop(0);
+ };
+
+ BranchGraph.prototype.appendLabel = function(x, y, commit) {
+ var label, r, rect, shortrefs, text, textbox, triangle;
+ if (!commit.refs) {
+ return;
+ }
+ r = this.r;
+ shortrefs = commit.refs;
+ if (shortrefs.length > 17) {
+ shortrefs = shortrefs.substr(0, 15) + "…";
+ }
+ text = r.text(x + 4, y, shortrefs).attr({
+ "text-anchor": "start",
+ font: "10px Monaco, monospace",
+ fill: "#FFF",
+ title: commit.refs
+ });
+ textbox = text.getBBox();
+ rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
+ });
+ triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
+ });
+ label = r.set(rect, text);
+ label.transform(["t", -rect.getBBox().width - 15, 0]);
+ return text.toFront();
+ };
+
+ BranchGraph.prototype.appendAnchor = function(x, y, commit) {
+ var anchor, options, r, top;
+ r = this.r;
+ top = this.top;
+ options = this.options;
+ anchor = r.circle(x, y, 10).attr({
+ fill: "#000",
+ opacity: 0,
+ cursor: "pointer"
+ }).click(function() {
+ return window.open(options.commit_url.replace("%s", commit.id), "_blank");
+ }).hover(function() {
+ this.tooltip = r.commitTooltip(x + 5, y, commit);
+ return top.push(this.tooltip.insertBefore(this));
+ }, function() {
+ return this.tooltip && this.tooltip.remove() && delete this.tooltip;
+ });
+ return top.push(anchor);
+ };
+
+ BranchGraph.prototype.drawDot = function(x, y, commit) {
+ var avatar_box_x, avatar_box_y, r;
+ r = this.r;
+ r.circle(x, y, 3).attr({
+ fill: this.colors[commit.space],
+ stroke: "none"
+ });
+ avatar_box_x = this.offsetX + this.unitSpace * this.mspace + 10;
+ avatar_box_y = y - 10;
+ r.rect(avatar_box_x, avatar_box_y, 20, 20).attr({
+ stroke: this.colors[commit.space],
+ "stroke-width": 2
+ });
+ r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20);
+ return r.text(this.offsetX + this.unitSpace * this.mspace + 35, y, commit.message.split("\n")[0]).attr({
+ "text-anchor": "start",
+ font: "14px Monaco, monospace"
+ });
+ };
+
+ BranchGraph.prototype.drawLines = function(x, y, commit) {
+ var arrow, color, i, j, len, offset, parent, parentCommit, parentX1, parentX2, parentY, r, ref, results, route;
+ r = this.r;
+ ref = commit.parents;
+ results = [];
+ for (i = j = 0, len = ref.length; j < len; i = ++j) {
+ parent = ref[i];
+ parentCommit = this.preparedCommits[parent[0]];
+ parentY = this.offsetY + this.unitTime * parentCommit.time;
+ parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
+ parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+ if (parentCommit.space <= commit.space) {
+ color = this.colors[commit.space];
+ } else {
+ color = this.colors[parentCommit.space];
+ }
+ if (parent[1] === commit.space) {
+ offset = [0, 5];
+ arrow = "l-2,5,4,0,-2,-5,0,5";
+ } else if (parent[1] < commit.space) {
+ offset = [3, 3];
+ arrow = "l5,0,-2,4,-3,-4,4,2";
+ } else {
+ offset = [-3, 3];
+ arrow = "l-5,0,2,4,3,-4,-4,2";
+ }
+ route = ["M", x + offset[0], y + offset[1]];
+ if (i > 0) {
+ route.push(arrow);
+ }
+ if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
+ route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
+ }
+ route.push("L", parentX1, parentY);
+ results.push(r.path(route).attr({
+ stroke: color,
+ "stroke-width": 2
+ }));
+ }
+ return results;
+ };
+
+ BranchGraph.prototype.markCommit = function(commit) {
+ var r, x, y;
+ if (commit.id === this.options.commit_id) {
+ r = this.r;
+ x = this.offsetX + this.unitSpace * (this.mspace - commit.space);
+ y = this.offsetY + this.unitTime * commit.time;
+ r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr({
+ fill: "#000",
+ "fill-opacity": .5,
+ stroke: "none"
+ });
+ return this.element.scrollTop(y - this.graphHeight / 2);
+ }
+ };
+
+ return BranchGraph;
+
+ })();
+
+ Raphael.prototype.commitTooltip = function(x, y, commit) {
+ var boxHeight, boxWidth, icon, idText, messageText, nameText, rect, textSet, tooltip;
+ boxWidth = 300;
+ boxHeight = 200;
+ icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20);
+ nameText = this.text(x + 25, y + 10, commit.author.name);
+ idText = this.text(x, y + 35, commit.id);
+ messageText = this.text(x, y + 50, commit.message);
+ textSet = this.set(icon, nameText, idText, messageText).attr({
+ "text-anchor": "start",
+ font: "12px Monaco, monospace"
+ });
+ nameText.attr({
+ font: "14px Arial",
+ "font-weight": "bold"
+ });
+ idText.attr({
+ fill: "#AAA"
+ });
+ this.textWrap(messageText, boxWidth - 50);
+ rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({
+ fill: "#FFF",
+ stroke: "#000",
+ "stroke-linecap": "round",
+ "stroke-width": 2
+ });
+ tooltip = this.set(rect, textSet);
+ rect.attr({
+ height: tooltip.getBBox().height + 10,
+ width: tooltip.getBBox().width + 10
+ });
+ tooltip.transform(["t", 20, 20]);
+ return tooltip;
+ };
+
+ Raphael.prototype.textWrap = function(t, width) {
+ var abc, b, content, h, j, len, letterWidth, s, word, words, x;
+ content = t.attr("text");
+ abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ t.attr({
+ text: abc
+ });
+ letterWidth = t.getBBox().width / abc.length;
+ t.attr({
+ text: content
+ });
+ words = content.split(" ");
+ x = 0;
+ s = [];
+ for (j = 0, len = words.length; j < len; j++) {
+ word = words[j];
+ if (x + (word.length * letterWidth) > width) {
+ s.push("\n");
+ x = 0;
+ }
+ x += word.length * letterWidth;
+ s.push(word + " ");
+ }
+ t.attr({
+ text: s.join("")
+ });
+ b = t.getBBox();
+ h = Math.abs(b.y2) - Math.abs(b.y) + 1;
+ return t.attr({
+ y: b.y + h
+ });
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/network/branch-graph.js.coffee b/app/assets/javascripts/network/branch-graph.js.coffee
deleted file mode 100644
index f2fd2a775a4..00000000000
--- a/app/assets/javascripts/network/branch-graph.js.coffee
+++ /dev/null
@@ -1,340 +0,0 @@
-class @BranchGraph
- constructor: (@element, @options) ->
- @preparedCommits = {}
- @mtime = 0
- @mspace = 0
- @parents = {}
- @colors = ["#000"]
- @offsetX = 150
- @offsetY = 20
- @unitTime = 30
- @unitSpace = 10
- @prev_start = -1
- @load()
-
- load: ->
- $.ajax
- url: @options.url
- method: "get"
- dataType: "json"
- success: $.proxy((data) ->
- $(".loading", @element).hide()
- @prepareData data.days, data.commits
- @buildGraph()
- , this)
-
- prepareData: (@days, @commits) ->
- @collectParents()
- @graphHeight = $(@element).height()
- @graphWidth = $(@element).width()
- ch = Math.max(@graphHeight, @offsetY + @unitTime * @mtime + 150)
- cw = Math.max(@graphWidth, @offsetX + @unitSpace * @mspace + 300)
- @r = Raphael(@element.get(0), cw, ch)
- @top = @r.set()
- @barHeight = Math.max(@graphHeight, @unitTime * @days.length + 320)
-
- for c in @commits
- c.isParent = true if c.id of @parents
- @preparedCommits[c.id] = c
- @markCommit(c)
-
- @collectColors()
-
- collectParents: ->
- for c in @commits
- @mtime = Math.max(@mtime, c.time)
- @mspace = Math.max(@mspace, c.space)
- for p in c.parents
- @parents[p[0]] = true
- @mspace = Math.max(@mspace, p[1])
-
- collectColors: ->
- k = 0
- while k < @mspace
- @colors.push Raphael.getColor(.8)
- # Skipping a few colors in the spectrum to get more contrast between colors
- Raphael.getColor()
- Raphael.getColor()
- k++
-
- buildGraph: ->
- r = @r
- cuday = 0
- cumonth = ""
-
- r.rect(0, 0, 40, @barHeight).attr fill: "#222"
- r.rect(40, 0, 30, @barHeight).attr fill: "#444"
-
- for day, mm in @days
- if cuday isnt day[0] || cumonth isnt day[1]
- # Dates
- r.text(55, @offsetY + @unitTime * mm, day[0])
- .attr(
- font: "12px Monaco, monospace"
- fill: "#BBB"
- )
- cuday = day[0]
-
- if cumonth isnt day[1]
- # Months
- r.text(20, @offsetY + @unitTime * mm, day[1])
- .attr(
- font: "12px Monaco, monospace"
- fill: "#EEE"
- )
- cumonth = day[1]
-
- @renderPartialGraph()
-
- @bindEvents()
-
- renderPartialGraph: ->
- start = Math.floor((@element.scrollTop() - @offsetY) / @unitTime) - 10
- if start < 0
- isGraphEdge = true
- start = 0
- end = start + 40
- if @commits.length < end
- isGraphEdge = true
- end = @commits.length
-
- if @prev_start == -1 or Math.abs(@prev_start - start) > 10 or isGraphEdge
- i = start
-
- @prev_start = start
-
- while i < end
- commit = @commits[i]
- i += 1
-
- if commit.hasDrawn isnt true
- x = @offsetX + @unitSpace * (@mspace - commit.space)
- y = @offsetY + @unitTime * commit.time
-
- @drawDot(x, y, commit)
-
- @drawLines(x, y, commit)
-
- @appendLabel(x, y, commit)
-
- @appendAnchor(x, y, commit)
-
- commit.hasDrawn = true
-
- @top.toFront()
-
- bindEvents: ->
- element = @element
-
- $(element).scroll (event) =>
- @renderPartialGraph()
-
- scrollDown: =>
- @element.scrollTop @element.scrollTop() + 50
- @renderPartialGraph()
-
- scrollUp: =>
- @element.scrollTop @element.scrollTop() - 50
- @renderPartialGraph()
-
- scrollLeft: =>
- @element.scrollLeft @element.scrollLeft() - 50
- @renderPartialGraph()
-
- scrollRight: =>
- @element.scrollLeft @element.scrollLeft() + 50
- @renderPartialGraph()
-
- scrollBottom: =>
- @element.scrollTop @element.find('svg').height()
-
- scrollTop: =>
- @element.scrollTop 0
-
- appendLabel: (x, y, commit) ->
- return unless commit.refs
-
- r = @r
- shortrefs = commit.refs
- # Truncate if longer than 15 chars
- shortrefs = shortrefs.substr(0, 15) + "…" if shortrefs.length > 17
- text = r.text(x + 4, y, shortrefs).attr(
- "text-anchor": "start"
- font: "10px Monaco, monospace"
- fill: "#FFF"
- title: commit.refs
- )
- textbox = text.getBBox()
- # Create rectangle based on the size of the textbox
- rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr(
- fill: "#000"
- "fill-opacity": .5
- stroke: "none"
- )
- triangle = r.path(["M", x - 5, y, "L", x - 15, y - 4, "L", x - 15, y + 4, "Z"]).attr(
- fill: "#000"
- "fill-opacity": .5
- stroke: "none"
- )
-
- label = r.set(rect, text)
- label.transform(["t", -rect.getBBox().width - 15, 0])
-
- # Set text to front
- text.toFront()
-
- appendAnchor: (x, y, commit) ->
- r = @r
- top = @top
- options = @options
- anchor = r.circle(x, y, 10).attr(
- fill: "#000"
- opacity: 0
- cursor: "pointer"
- ).click(->
- window.open options.commit_url.replace("%s", commit.id), "_blank"
- ).hover(->
- @tooltip = r.commitTooltip(x + 5, y, commit)
- top.push @tooltip.insertBefore(this)
- , ->
- @tooltip and @tooltip.remove() and delete @tooltip
- )
- top.push anchor
-
- drawDot: (x, y, commit) ->
- r = @r
- r.circle(x, y, 3).attr(
- fill: @colors[commit.space]
- stroke: "none"
- )
-
- avatar_box_x = @offsetX + @unitSpace * @mspace + 10
- avatar_box_y = y - 10
- r.rect(avatar_box_x, avatar_box_y, 20, 20).attr(
- stroke: @colors[commit.space]
- "stroke-width": 2
- )
- r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20)
- r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr(
- "text-anchor": "start"
- font: "14px Monaco, monospace"
- )
-
- drawLines: (x, y, commit) ->
- r = @r
- for parent, i in commit.parents
- parentCommit = @preparedCommits[parent[0]]
- parentY = @offsetY + @unitTime * parentCommit.time
- parentX1 = @offsetX + @unitSpace * (@mspace - parentCommit.space)
- parentX2 = @offsetX + @unitSpace * (@mspace - parent[1])
-
- # Set line color
- if parentCommit.space <= commit.space
- color = @colors[commit.space]
-
- else
- color = @colors[parentCommit.space]
-
- # Build line shape
- if parent[1] is commit.space
- offset = [0, 5]
- arrow = "l-2,5,4,0,-2,-5,0,5"
-
- else if parent[1] < commit.space
- offset = [3, 3]
- arrow = "l5,0,-2,4,-3,-4,4,2"
-
- else
- offset = [-3, 3]
- arrow = "l-5,0,2,4,3,-4,-4,2"
-
- # Start point
- route = ["M", x + offset[0], y + offset[1]]
-
- # Add arrow if not first parent
- if i > 0
- route.push(arrow)
-
- # Circumvent if overlap
- if commit.space isnt parentCommit.space or commit.space isnt parent[1]
- route.push(
- "L", parentX2, y + 10,
- "L", parentX2, parentY - 5,
- )
-
- # End point
- route.push("L", parentX1, parentY)
-
- r
- .path(route)
- .attr(
- stroke: color
- "stroke-width": 2)
-
- markCommit: (commit) ->
- if commit.id is @options.commit_id
- r = @r
- x = @offsetX + @unitSpace * (@mspace - commit.space)
- y = @offsetY + @unitTime * commit.time
- r.path(["M", x + 5, y, "L", x + 15, y + 4, "L", x + 15, y - 4, "Z"]).attr(
- fill: "#000"
- "fill-opacity": .5
- stroke: "none"
- )
- # Displayed in the center
- @element.scrollTop(y - @graphHeight / 2)
-
-Raphael::commitTooltip = (x, y, commit) ->
- boxWidth = 300
- boxHeight = 200
- icon = @image(gon.relative_url_root + commit.author.icon, x, y, 20, 20)
- nameText = @text(x + 25, y + 10, commit.author.name)
- idText = @text(x, y + 35, commit.id)
- messageText = @text(x, y + 50, commit.message)
- textSet = @set(icon, nameText, idText, messageText).attr(
- "text-anchor": "start"
- font: "12px Monaco, monospace"
- )
- nameText.attr(
- font: "14px Arial"
- "font-weight": "bold"
- )
-
- idText.attr fill: "#AAA"
- @textWrap messageText, boxWidth - 50
- rect = @rect(x - 10, y - 10, boxWidth, 100, 4).attr(
- fill: "#FFF"
- stroke: "#000"
- "stroke-linecap": "round"
- "stroke-width": 2
- )
- tooltip = @set(rect, textSet)
- rect.attr(
- height: tooltip.getBBox().height + 10
- width: tooltip.getBBox().width + 10
- )
-
- tooltip.transform ["t", 20, 20]
- tooltip
-
-Raphael::textWrap = (t, width) ->
- content = t.attr("text")
- abc = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
- t.attr text: abc
- letterWidth = t.getBBox().width / abc.length
- t.attr text: content
- words = content.split(" ")
- x = 0
- s = []
-
- for word in words
- if x + (word.length * letterWidth) > width
- s.push "\n"
- x = 0
- x += word.length * letterWidth
- s.push word + " "
-
- t.attr text: s.join("")
- b = t.getBBox()
- h = Math.abs(b.y2) - Math.abs(b.y) + 1
- t.attr y: b.y + h
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
new file mode 100644
index 00000000000..7baebcd100a
--- /dev/null
+++ b/app/assets/javascripts/network/network.js
@@ -0,0 +1,19 @@
+(function() {
+ this.Network = (function() {
+ function Network(opts) {
+ var vph;
+ $("#filter_ref").click(function() {
+ return $(this).closest('form').submit();
+ });
+ this.branch_graph = new BranchGraph($(".network-graph"), opts);
+ vph = $(window).height() - 250;
+ $('.network-graph').css({
+ 'height': vph + 'px'
+ });
+ }
+
+ return Network;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/network/network.js.coffee b/app/assets/javascripts/network/network.js.coffee
deleted file mode 100644
index f4ef07a50a7..00000000000
--- a/app/assets/javascripts/network/network.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-class @Network
- constructor: (opts) ->
- $("#filter_ref").click ->
- $(this).closest('form').submit()
-
- @branch_graph = new BranchGraph($(".network-graph"), opts)
-
- vph = $(window).height() - 250
- $('.network-graph').css 'height': (vph + 'px')
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
new file mode 100644
index 00000000000..6a7422a7755
--- /dev/null
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -0,0 +1,16 @@
+
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var network_graph;
+ network_graph = new Network({
+ url: $(".network-graph").attr('data-url'),
+ commit_url: $(".network-graph").attr('data-commit-url'),
+ ref: $(".network-graph").attr('data-ref'),
+ commit_id: $(".network-graph").attr('data-commit-id')
+ });
+ return new ShortcutsNetwork(network_graph.branch_graph);
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
new file mode 100644
index 00000000000..20aa2fced27
--- /dev/null
+++ b/app/assets/javascripts/new_branch_form.js
@@ -0,0 +1,104 @@
+(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; };
+
+ this.NewBranchForm = (function() {
+ function NewBranchForm(form, availableRefs) {
+ this.validate = bind(this.validate, this);
+ this.branchNameError = form.find('.js-branch-name-error');
+ this.name = form.find('.js-branch-name');
+ this.ref = form.find('#ref');
+ this.setupAvailableRefs(availableRefs);
+ this.setupRestrictions();
+ this.addBinding();
+ this.init();
+ }
+
+ NewBranchForm.prototype.addBinding = function() {
+ return this.name.on('blur', this.validate);
+ };
+
+ NewBranchForm.prototype.init = function() {
+ if (this.name.val().length > 0) {
+ return this.name.trigger('blur');
+ }
+ };
+
+ NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) {
+ return this.ref.autocomplete({
+ source: availableRefs,
+ minLength: 1
+ });
+ };
+
+ NewBranchForm.prototype.setupRestrictions = function() {
+ var endsWith, invalid, single, startsWith;
+ startsWith = {
+ pattern: /^(\/|\.)/g,
+ prefix: "can't start with",
+ conjunction: "or"
+ };
+ endsWith = {
+ pattern: /(\/|\.|\.lock)$/g,
+ prefix: "can't end in",
+ conjunction: "or"
+ };
+ invalid = {
+ pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g,
+ prefix: "can't contain",
+ conjunction: ", "
+ };
+ single = {
+ pattern: /^@+$/g,
+ prefix: "can't be",
+ conjunction: "or"
+ };
+ return this.restrictions = [startsWith, invalid, endsWith, single];
+ };
+
+ NewBranchForm.prototype.validate = function() {
+ var errorMessage, errors, formatter, unique, validator;
+ this.branchNameError.empty();
+ unique = function(values, value) {
+ if (indexOf.call(values, value) < 0) {
+ values.push(value);
+ }
+ return values;
+ };
+ formatter = function(values, restriction) {
+ var formatted;
+ formatted = values.map(function(value) {
+ switch (false) {
+ case !/\s/.test(value):
+ return 'spaces';
+ case !/\/{2,}/g.test(value):
+ return 'consecutive slashes';
+ default:
+ return "'" + value + "'";
+ }
+ });
+ return restriction.prefix + " " + (formatted.join(restriction.conjunction));
+ };
+ validator = (function(_this) {
+ return function(errors, restriction) {
+ var matched;
+ matched = _this.name.val().match(restriction.pattern);
+ if (matched) {
+ return errors.concat(formatter(matched.reduce(unique, []), restriction));
+ } else {
+ return errors;
+ }
+ };
+ })(this);
+ errors = this.restrictions.reduce(validator, []);
+ if (errors.length > 0) {
+ errorMessage = $("<span/>").text(errors.join(', '));
+ return this.branchNameError.append(errorMessage);
+ }
+ };
+
+ return NewBranchForm;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee
deleted file mode 100644
index 4b350854f78..00000000000
--- a/app/assets/javascripts/new_branch_form.js.coffee
+++ /dev/null
@@ -1,78 +0,0 @@
-class @NewBranchForm
- constructor: (form, availableRefs) ->
- @branchNameError = form.find('.js-branch-name-error')
- @name = form.find('.js-branch-name')
- @ref = form.find('#ref')
-
- @setupAvailableRefs(availableRefs)
- @setupRestrictions()
- @addBinding()
- @init()
-
- addBinding: ->
- @name.on 'blur', @validate
-
- init: ->
- @name.trigger 'blur' if @name.val().length > 0
-
- setupAvailableRefs: (availableRefs) ->
- @ref.autocomplete
- source: availableRefs,
- minLength: 1
-
- setupRestrictions: ->
- startsWith = {
- pattern: /^(\/|\.)/g,
- prefix: "can't start with",
- conjunction: "or"
- }
-
- endsWith = {
- pattern: /(\/|\.|\.lock)$/g,
- prefix: "can't end in",
- conjunction: "or"
- }
-
- invalid = {
- pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g
- prefix: "can't contain",
- conjunction: ", "
- }
-
- single = {
- pattern: /^@+$/g
- prefix: "can't be",
- conjunction: "or"
- }
-
- @restrictions = [startsWith, invalid, endsWith, single]
-
- validate: =>
- @branchNameError.empty()
-
- unique = (values, value) ->
- values.push(value) unless value in values
- values
-
- formatter = (values, restriction) ->
- formatted = values.map (value) ->
- switch
- when /\s/.test value then 'spaces'
- when /\/{2,}/g.test value then 'consecutive slashes'
- else "'#{value}'"
-
- "#{restriction.prefix} #{formatted.join(restriction.conjunction)}"
-
- validator = (errors, restriction) =>
- matched = @name.val().match(restriction.pattern)
-
- if matched
- errors.concat formatter(matched.reduce(unique, []), restriction)
- else
- errors
-
- errors = @restrictions.reduce validator, []
-
- if errors.length > 0
- errorMessage = $("<span/>").text(errors.join(', '))
- @branchNameError.append(errorMessage)
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
new file mode 100644
index 00000000000..21bf8867f7b
--- /dev/null
+++ b/app/assets/javascripts/new_commit_form.js
@@ -0,0 +1,34 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.NewCommitForm = (function() {
+ function NewCommitForm(form) {
+ this.renderDestination = bind(this.renderDestination, this);
+ this.newBranch = form.find('.js-target-branch');
+ this.originalBranch = form.find('.js-original-branch');
+ this.createMergeRequest = form.find('.js-create-merge-request');
+ this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.renderDestination();
+ this.newBranch.keyup(this.renderDestination);
+ }
+
+ NewCommitForm.prototype.renderDestination = function() {
+ var different;
+ different = this.newBranch.val() !== this.originalBranch.val();
+ if (different) {
+ this.createMergeRequestContainer.show();
+ if (!this.wasDifferent) {
+ this.createMergeRequest.prop('checked', true);
+ }
+ } else {
+ this.createMergeRequestContainer.hide();
+ this.createMergeRequest.prop('checked', false);
+ }
+ return this.wasDifferent = different;
+ };
+
+ return NewCommitForm;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee
deleted file mode 100644
index 03f0f51acfa..00000000000
--- a/app/assets/javascripts/new_commit_form.js.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-class @NewCommitForm
- constructor: (form) ->
- @newBranch = form.find('.js-target-branch')
- @originalBranch = form.find('.js-original-branch')
- @createMergeRequest = form.find('.js-create-merge-request')
- @createMergeRequestContainer = form.find('.js-create-merge-request-container')
-
- @renderDestination()
- @newBranch.keyup @renderDestination
-
- renderDestination: =>
- different = @newBranch.val() != @originalBranch.val()
-
- if different
- @createMergeRequestContainer.show()
- @createMergeRequest.prop('checked', true) unless @wasDifferent
- else
- @createMergeRequestContainer.hide()
- @createMergeRequest.prop('checked', false)
-
- @wasDifferent = different
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
new file mode 100644
index 00000000000..d0d5cad813a
--- /dev/null
+++ b/app/assets/javascripts/notes.js
@@ -0,0 +1,810 @@
+
+/*= require autosave */
+
+
+/*= require autosize */
+
+
+/*= require dropzone */
+
+
+/*= require dropzone_input */
+
+
+/*= require gfm_auto_complete */
+
+
+/*= require jquery.atwho */
+
+
+/*= require task_list */
+
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Notes = (function() {
+ var isMetaKey;
+
+ Notes.interval = null;
+
+ function Notes(notes_url, note_ids, last_fetched_at, view) {
+ this.updateTargetButtons = bind(this.updateTargetButtons, this);
+ this.updateCloseButton = bind(this.updateCloseButton, this);
+ this.visibilityChange = bind(this.visibilityChange, this);
+ this.cancelDiscussionForm = bind(this.cancelDiscussionForm, this);
+ this.addDiffNote = bind(this.addDiffNote, this);
+ this.setupDiscussionNoteForm = bind(this.setupDiscussionNoteForm, this);
+ this.replyToDiscussionNote = bind(this.replyToDiscussionNote, this);
+ this.removeNote = bind(this.removeNote, this);
+ this.cancelEdit = bind(this.cancelEdit, this);
+ this.updateNote = bind(this.updateNote, this);
+ this.addDiscussionNote = bind(this.addDiscussionNote, this);
+ this.addNoteError = bind(this.addNoteError, this);
+ this.addNote = bind(this.addNote, this);
+ this.resetMainTargetForm = bind(this.resetMainTargetForm, this);
+ this.refresh = bind(this.refresh, this);
+ this.keydownNoteText = bind(this.keydownNoteText, this);
+ this.notes_url = notes_url;
+ this.note_ids = note_ids;
+ this.last_fetched_at = last_fetched_at;
+ this.view = view;
+ this.noteable_url = document.URL;
+ this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge"));
+ this.basePollingInterval = 15000;
+ this.maxPollingSteps = 4;
+ this.cleanBinding();
+ this.addBinding();
+ this.setPollingInterval();
+ this.setupMainTargetNoteForm();
+ this.initTaskList();
+ }
+
+ Notes.prototype.addBinding = function() {
+ $(document).on("ajax:success", ".js-main-target-form", this.addNote);
+ $(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
+ $(document).on("ajax:error", ".js-main-target-form", this.addNoteError);
+ $(document).on("ajax:success", "form.edit-note", this.updateNote);
+ $(document).on("click", ".js-note-edit", this.showEditForm);
+ $(document).on("click", ".note-edit-cancel", this.cancelEdit);
+ $(document).on("click", ".js-comment-button", this.updateCloseButton);
+ $(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ $(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
+ $(document).on("click", ".js-note-delete", this.removeNote);
+ $(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+ $(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
+ $(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
+ $(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+ $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+ $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
+ $(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
+ $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ $(document).on("visibilitychange", this.visibilityChange);
+ $(document).on("issuable:change", this.refresh);
+ return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
+ };
+
+ Notes.prototype.cleanBinding = function() {
+ $(document).off("ajax:success", ".js-main-target-form");
+ $(document).off("ajax:success", ".js-discussion-note-form");
+ $(document).off("ajax:success", "form.edit-note");
+ $(document).off("click", ".js-note-edit");
+ $(document).off("click", ".note-edit-cancel");
+ $(document).off("click", ".js-note-delete");
+ $(document).off("click", ".js-note-attachment-delete");
+ $(document).off("ajax:complete", ".js-main-target-form");
+ $(document).off("ajax:success", ".js-main-target-form");
+ $(document).off("click", ".js-discussion-reply-button");
+ $(document).off("click", ".js-add-diff-note-button");
+ $(document).off("visibilitychange");
+ $(document).off("keyup", ".js-note-text");
+ $(document).off("click", ".js-note-target-reopen");
+ $(document).off("click", ".js-note-target-close");
+ $(document).off("click", ".js-note-discard");
+ $(document).off("keydown", ".js-note-text");
+ $(document).off('click', '.js-comment-resolve-button');
+ $('.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)) {
+ return;
+ }
+ $textarea = $(e.target);
+ switch (e.which) {
+ case 38:
+ if ($textarea.val() !== '') {
+ return;
+ }
+ myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last");
+ if (myLastNote.length) {
+ myLastNoteEditBtn = myLastNote.find('.js-note-edit');
+ return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
+ }
+ break;
+ case 27:
+ discussionNoteForm = $textarea.closest('.js-discussion-note-form');
+ if (discussionNoteForm.length) {
+ if ($textarea.val() !== '') {
+ if (!confirm('Are you sure you want to cancel creating this comment?')) {
+ return;
+ }
+ }
+ this.removeDiscussionNoteForm(discussionNoteForm);
+ return;
+ }
+ editNote = $textarea.closest('.note');
+ if (editNote.length) {
+ originalText = $textarea.closest('form').data('original-note');
+ newText = $textarea.val();
+ if (originalText !== newText) {
+ if (!confirm('Are you sure you want to cancel editing this comment?')) {
+ return;
+ }
+ }
+ return this.removeNoteEditForm(editNote);
+ }
+ }
+ };
+
+ 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) {
+ return function() {
+ return _this.refresh();
+ };
+ })(this), this.pollingInterval);
+ };
+
+ Notes.prototype.refresh = function() {
+ if (!document.hidden && document.URL.indexOf(this.noteable_url) === 0) {
+ return this.getContent();
+ }
+ };
+
+ Notes.prototype.getContent = function() {
+ if (this.refreshing) {
+ return;
+ }
+ this.refreshing = true;
+ return $.ajax({
+ url: this.notes_url,
+ data: "last_fetched_at=" + this.last_fetched_at,
+ dataType: "json",
+ success: (function(_this) {
+ return function(data) {
+ var notes;
+ notes = data.notes;
+ _this.last_fetched_at = data.last_fetched_at;
+ _this.setPollingInterval(data.notes.length);
+ return $.each(notes, function(i, note) {
+ if (note.discussion_html != null) {
+ return _this.renderDiscussionNote(note);
+ } else {
+ return _this.renderNote(note);
+ }
+ });
+ };
+ })(this)
+ }).always((function(_this) {
+ return function() {
+ return _this.refreshing = false;
+ };
+ })(this));
+ };
+
+
+ /*
+ Increase @pollingInterval up to 120 seconds on every function call,
+ if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
+ will reset to @basePollingInterval.
+
+ Note: this function is used to gradually increase the polling interval
+ if there aren't new notes coming from the server
+ */
+
+ Notes.prototype.setPollingInterval = function(shouldReset) {
+ var nthInterval;
+ if (shouldReset == null) {
+ shouldReset = true;
+ }
+ nthInterval = this.basePollingInterval * Math.pow(2, this.maxPollingSteps - 1);
+ if (shouldReset) {
+ this.pollingInterval = this.basePollingInterval;
+ } else if (this.pollingInterval < nthInterval) {
+ this.pollingInterval *= 2;
+ }
+ return this.initRefresh();
+ };
+
+
+ /*
+ Render note in main comments area.
+
+ Note: for rendering inline notes use renderDiscussionNote
+ */
+
+ Notes.prototype.renderNote = function(note) {
+ var $notesList, votesBlock;
+ if (!note.valid) {
+ if (note.award) {
+ new Flash('You have already awarded this emoji!', 'alert', this.parentTimeline);
+ }
+ else {
+ if (note.errors.commands_only) {
+ new Flash(note.errors.commands_only, 'notice', this.parentTimeline);
+ this.refresh();
+ }
+ }
+ return;
+ }
+ if (note.award) {
+ votesBlock = $('.js-awards-block').eq(0);
+ gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
+ return gl.awardsHandler.scrollToAwards();
+ } else if (this.isNewNote(note)) {
+ this.note_ids.push(note.id);
+ $notesList = $('ul.main-notes-list');
+ $notesList.append(note.html).syntaxHighlight();
+ gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
+ this.initTaskList();
+ this.refresh();
+ return this.updateNotesCount(1);
+ }
+ };
+
+
+ /*
+ Check if note does not exists on page
+ */
+
+ Notes.prototype.isNewNote = function(note) {
+ return $.inArray(note.id, this.note_ids) === -1;
+ };
+
+ Notes.prototype.isParallelView = function() {
+ return this.view === 'parallel';
+ };
+
+
+ /*
+ Render note in discussion area.
+
+ Note: for rendering inline notes use renderDiscussionNote
+ */
+
+ Notes.prototype.renderDiscussionNote = function(note) {
+ var discussionContainer, form, note_html, row;
+ if (!this.isNewNote(note)) {
+ return;
+ }
+ this.note_ids.push(note.id);
+ form = $("#new-discussion-note-form-" + note.discussion_id);
+ if ((note.original_discussion_id != null) && form.length === 0) {
+ form = $("#new-discussion-note-form-" + note.original_discussion_id);
+ }
+ row = form.closest("tr");
+ note_html = $(note.html);
+ note_html.syntaxHighlight();
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
+ discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
+ }
+ if (discussionContainer.length === 0) {
+ row.after(note.diff_discussion_html);
+ row.next().find(".note").remove();
+ discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ discussionContainer.append(note_html);
+ if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
+ $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
+ }
+ } else {
+ discussionContainer.append(note_html);
+ }
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+
+ gl.utils.localTimeAgo($('.js-timeago', note_html), false);
+ return this.updateNotesCount(1);
+ };
+
+
+ /*
+ Called in response the main target form has been successfully submitted.
+
+ Removes any errors.
+ Resets text and preview.
+ Resets buttons.
+ */
+
+ Notes.prototype.resetMainTargetForm = function(e) {
+ var form;
+ form = $(".js-main-target-form");
+ form.find(".js-errors").remove();
+ form.find(".js-md-write-button").click();
+ form.find(".js-note-text").val("").trigger("input");
+ form.find(".js-note-text").data("autosave").reset();
+ return this.updateTargetButtons(e);
+ };
+
+ Notes.prototype.reenableTargetFormSubmitButton = function() {
+ var form;
+ form = $(".js-main-target-form");
+ return form.find(".js-note-text").trigger("input");
+ };
+
+
+ /*
+ Shows the main form and does some setup on it.
+
+ Sets some hidden fields in the form.
+ */
+
+ Notes.prototype.setupMainTargetNoteForm = function() {
+ var form;
+ form = $(".js-new-note-form");
+ this.formClone = form.clone();
+ this.setupNoteForm(form);
+ form.removeClass("js-new-note-form");
+ form.addClass("js-main-target-form");
+ form.find("#note_line_code").remove();
+ form.find("#note_position").remove();
+ form.find("#note_type").remove();
+ form.find('.js-comment-resolve-button').closest('comment-and-resolve-btn').remove();
+ return this.parentTimeline = form.parents('.timeline');
+ };
+
+
+ /*
+ General note form setup.
+
+ deactivates the submit button when text is empty
+ hides the preview button when text is empty
+ setup GFM auto complete
+ show the form
+ */
+
+ Notes.prototype.setupNoteForm = function(form) {
+ var textarea;
+ new GLForm(form);
+ textarea = form.find(".js-note-text");
+ return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
+ };
+
+
+ /*
+ Called in response to the new note form being submitted
+
+ Adds new note to list.
+ */
+
+ Notes.prototype.addNote = function(xhr, note, status) {
+ return this.renderNote(note);
+ };
+
+ Notes.prototype.addNoteError = function(xhr, note, status) {
+ return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
+ };
+
+
+ /*
+ Called in response to the new note form being submitted
+
+ Adds new note to list.
+ */
+
+ Notes.prototype.addDiscussionNote = function(xhr, note, status) {
+ var $form = $(xhr.target);
+
+ if ($form.attr('data-resolve-all') != null) {
+ var namespacePath = $form.attr('data-namespace-path'),
+ projectPath = $form.attr('data-project-path')
+ discussionId = $form.attr('data-discussion-id'),
+ mergeRequestId = $form.attr('data-noteable-iid'),
+ namespace = namespacePath + '/' + projectPath;
+
+ if (ResolveService != null) {
+ ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ }
+ }
+
+ this.renderDiscussionNote(note);
+ this.removeDiscussionNoteForm($form);
+ };
+
+
+ /*
+ Called in response to the edit note form being submitted
+
+ Updates the current note field.
+ */
+
+ Notes.prototype.updateNote = function(_xhr, note, _status) {
+ var $html, $note_li;
+ $html = $(note.html);
+ gl.utils.localTimeAgo($('.js-timeago', $html));
+ $html.syntaxHighlight();
+ $html.find('.js-task-list-container').taskList('enable');
+ $note_li = $('.note-row-' + note.id);
+
+ $note_li.replaceWith($html);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+ };
+
+
+ /*
+ Called in response to clicking the edit note link
+
+ Replaces the note text with the note edit form
+ Adds a data attribute to the form with the original content of the note for cancellations
+ */
+
+ Notes.prototype.showEditForm = function(e, scrollTo, myLastNote) {
+ var $noteText, done, form, note;
+ e.preventDefault();
+ note = $(this).closest(".note");
+ note.addClass("is-editting");
+ form = note.find(".note-edit-form");
+ form.addClass('current-note-edit-form');
+ note.find(".js-note-attachment-delete").show();
+ done = function($noteText) {
+ var noteTextVal;
+ noteTextVal = $noteText.val();
+ form.find('form.edit-note').data('original-note', noteTextVal);
+ return $noteText.val('').val(noteTextVal);
+ };
+ new GLForm(form);
+ if ((scrollTo != null) && (myLastNote != null)) {
+ $('html, body').scrollTop($(document).height());
+ return $('html, body').animate({
+ scrollTop: myLastNote.offset().top - 150
+ }, 500, function() {
+ var $noteText;
+ $noteText = form.find(".js-note-text");
+ $noteText.focus();
+ return done($noteText);
+ });
+ } else {
+ $noteText = form.find('.js-note-text');
+ $noteText.focus();
+ return done($noteText);
+ }
+ };
+
+
+ /*
+ Called in response to clicking the edit note link
+
+ Hides edit form and restores the original note text to the editor textarea.
+ */
+
+ Notes.prototype.cancelEdit = function(e) {
+ var note;
+ e.preventDefault();
+ note = $(e.target).closest('.note');
+ return this.removeNoteEditForm(note);
+ };
+
+ Notes.prototype.removeNoteEditForm = function(note) {
+ var form;
+ form = note.find(".current-note-edit-form");
+ note.removeClass("is-editting");
+ form.removeClass("current-note-edit-form");
+ return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
+ };
+
+
+ /*
+ Called in response to deleting a note of any kind.
+
+ Removes the actual note from view.
+ Removes the whole discussion if the last note is being removed.
+ */
+
+ Notes.prototype.removeNote = function(e) {
+ var noteId;
+ noteId = $(e.currentTarget).closest(".note").attr("id");
+ $(".note[id='" + noteId + "']").each((function(_this) {
+ return function(i, el) {
+ var note, notes;
+ note = $(el);
+ notes = note.closest(".notes");
+
+ if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
+ ref = DiffNotesApp.$refs[noteId];
+
+ if (ref) {
+ ref.$destroy(true);
+ }
+ }
+
+ if (notes.find(".note").length === 1) {
+ notes.closest(".timeline-entry").remove();
+ notes.closest("tr").remove();
+ }
+ return note.remove();
+ };
+ })(this));
+ return this.updateNotesCount(-1);
+ };
+
+
+ /*
+ Called in response to clicking the delete attachment link
+
+ Removes the attachment wrapper view, including image tag if it exists
+ Resets the note editing form
+ */
+
+ Notes.prototype.removeAttachment = function() {
+ var note;
+ note = $(this).closest(".note");
+ note.find(".note-attachment").remove();
+ note.find(".note-body > .note-text").show();
+ note.find(".note-header").show();
+ return note.find(".current-note-edit-form").remove();
+ };
+
+
+ /*
+ Called when clicking on the "reply" button for a diff line.
+
+ Shows the note form below the notes.
+ */
+
+ Notes.prototype.replyToDiscussionNote = function(e) {
+ var form, replyLink;
+ form = this.formClone.clone();
+ replyLink = $(e.target).closest(".js-discussion-reply-button");
+ replyLink
+ .closest('.discussion-reply-holder')
+ .hide()
+ .after(form);
+ return this.setupDiscussionNoteForm(replyLink, form);
+ };
+
+
+ /*
+ Shows the diff or discussion form and does some setup on it.
+
+ Sets some hidden fields in the form.
+
+ Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
+ and "noteableId" data attributes set.
+ */
+
+ Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
+ form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
+ form.attr("data-line-code", dataHolder.data("lineCode"));
+ form.find("#note_type").val(dataHolder.data("noteType"));
+ form.find("#line_type").val(dataHolder.data("lineType"));
+ form.find("#note_commit_id").val(dataHolder.data("commitId"));
+ form.find("#note_line_code").val(dataHolder.data("lineCode"));
+ form.find("#note_position").val(dataHolder.attr("data-position"));
+ form.find("#note_noteable_type").val(dataHolder.data("noteableType"));
+ form.find("#note_noteable_id").val(dataHolder.data("noteableId"));
+ form.find('.js-note-discard').show().removeClass('js-note-discard').addClass('js-close-discussion-note-form').text(form.find('.js-close-discussion-note-form').data('cancel-text'));
+ form.find('.js-note-target-close').remove();
+ this.setupNoteForm(form);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ var $commentBtn = form.find('comment-and-resolve-btn');
+ $commentBtn
+ .attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
+ DiffNotesApp.$compile($commentBtn.get(0));
+ }
+
+ form.find(".js-note-text").focus();
+ form
+ .find('.js-comment-resolve-button')
+ .attr('data-discussion-id', dataHolder.data('discussionId'));
+ form
+ .removeClass('js-main-target-form')
+ .addClass("discussion-form js-discussion-note-form");
+ };
+
+
+ /*
+ Called when clicking on the "add a comment" button on the side of a diff line.
+
+ Inserts a temporary row for the form below the line.
+ Sets up the form and shows it.
+ */
+
+ Notes.prototype.addDiffNote = function(e) {
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent;
+ e.preventDefault();
+ $link = $(e.currentTarget);
+ row = $link.closest("tr");
+ nextRow = row.next();
+ hasNotes = nextRow.is(".notes_holder");
+ addForm = false;
+ notesContentSelector = ".notes_content";
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ if (this.isParallelView()) {
+ lineType = $link.data("lineType");
+ notesContentSelector += "." + lineType;
+ rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
+ }
+ notesContentSelector += " .content";
+ if (hasNotes) {
+ nextRow.show();
+ notesContent = nextRow.find(notesContentSelector);
+ if (notesContent.length) {
+ notesContent.show();
+ replyButton = notesContent.find(".js-discussion-reply-button:visible");
+ if (replyButton.length) {
+ e.target = replyButton[0];
+ $.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
+ } else {
+ noteForm = notesContent.find(".js-discussion-note-form");
+ if (noteForm.length === 0) {
+ addForm = true;
+ }
+ }
+ }
+ } else {
+ row.after(rowCssToAdd);
+ nextRow = row.next();
+ notesContent = nextRow.find(notesContentSelector);
+ addForm = true;
+ }
+ if (addForm) {
+ newForm = this.formClone.clone();
+ newForm.appendTo(notesContent);
+ return this.setupDiscussionNoteForm($link, newForm);
+ }
+ };
+
+
+ /*
+ Called in response to "cancel" on a diff note form.
+
+ Shows the reply button again.
+ Removes the form and if necessary it's temporary row.
+ */
+
+ Notes.prototype.removeDiscussionNoteForm = function(form) {
+ var glForm, row;
+ row = form.closest("tr");
+ glForm = form.data('gl-form');
+ glForm.destroy();
+ form.find(".js-note-text").data("autosave").reset();
+ form
+ .prev('.discussion-reply-holder')
+ .show();
+ if (row.is(".js-temp-notes-holder")) {
+ return row.remove();
+ } else {
+ return form.remove();
+ }
+ };
+
+ Notes.prototype.cancelDiscussionForm = function(e) {
+ var form;
+ e.preventDefault();
+ form = $(e.target).closest(".js-discussion-note-form");
+ return this.removeDiscussionNoteForm(form);
+ };
+
+
+ /*
+ Called after an attachment file has been selected.
+
+ Updates the file name for the selected attachment.
+ */
+
+ Notes.prototype.updateFormAttachment = function() {
+ var filename, form;
+ form = $(this).closest("form");
+ filename = $(this).val().replace(/^.*[\\\/]/, "");
+ return form.find(".js-attachment-filename").text(filename);
+ };
+
+
+ /*
+ Called when the tab visibility changes
+ */
+
+ Notes.prototype.visibilityChange = function() {
+ return this.refresh();
+ };
+
+ Notes.prototype.updateCloseButton = function(e) {
+ var closebtn, form, textarea;
+ textarea = $(e.target);
+ form = textarea.parents('form');
+ closebtn = form.find('.js-note-target-close');
+ return closebtn.text(closebtn.data('original-text'));
+ };
+
+ Notes.prototype.updateTargetButtons = function(e) {
+ var closebtn, closetext, discardbtn, form, reopenbtn, reopentext, textarea;
+ textarea = $(e.target);
+ form = textarea.parents('form');
+ reopenbtn = form.find('.js-note-target-reopen');
+ closebtn = form.find('.js-note-target-close');
+ discardbtn = form.find('.js-note-discard');
+ if (textarea.val().trim().length > 0) {
+ reopentext = reopenbtn.data('alternative-text');
+ closetext = closebtn.data('alternative-text');
+ if (reopenbtn.text() !== reopentext) {
+ reopenbtn.text(reopentext);
+ }
+ if (closebtn.text() !== closetext) {
+ closebtn.text(closetext);
+ }
+ if (reopenbtn.is(':not(.btn-comment-and-reopen)')) {
+ reopenbtn.addClass('btn-comment-and-reopen');
+ }
+ if (closebtn.is(':not(.btn-comment-and-close)')) {
+ closebtn.addClass('btn-comment-and-close');
+ }
+ if (discardbtn.is(':hidden')) {
+ return discardbtn.show();
+ }
+ } else {
+ reopentext = reopenbtn.data('original-text');
+ closetext = closebtn.data('original-text');
+ if (reopenbtn.text() !== reopentext) {
+ reopenbtn.text(reopentext);
+ }
+ if (closebtn.text() !== closetext) {
+ closebtn.text(closetext);
+ }
+ if (reopenbtn.is('.btn-comment-and-reopen')) {
+ reopenbtn.removeClass('btn-comment-and-reopen');
+ }
+ if (closebtn.is('.btn-comment-and-close')) {
+ closebtn.removeClass('btn-comment-and-close');
+ }
+ if (discardbtn.is(':visible')) {
+ return discardbtn.hide();
+ }
+ }
+ };
+
+ Notes.prototype.initTaskList = function() {
+ this.enableTaskList();
+ return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList);
+ };
+
+ Notes.prototype.enableTaskList = function() {
+ return $('.note .js-task-list-container').taskList('enable');
+ };
+
+ Notes.prototype.updateTaskList = function() {
+ return $('form', this).submit();
+ };
+
+ Notes.prototype.updateNotesCount = function(updateCount) {
+ return this.notesCountBadge.text(parseInt(this.notesCountBadge.text()) + updateCount);
+ };
+
+ Notes.prototype.resolveDiscussion = function () {
+ var $this = $(this),
+ discussionId = $this.attr('data-discussion-id');
+
+ $this
+ .closest('form')
+ .attr('data-discussion-id', discussionId)
+ .attr('data-resolve-all', 'true')
+ .attr('data-namespace-path', $this.attr('data-namespace-path'))
+ .attr('data-project-path', $this.attr('data-project-path'));
+ };
+
+ return Notes;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
deleted file mode 100644
index 0b7d8f64456..00000000000
--- a/app/assets/javascripts/notes.js.coffee
+++ /dev/null
@@ -1,694 +0,0 @@
-#= require autosave
-#= require autosize
-#= require dropzone
-#= require dropzone_input
-#= require gfm_auto_complete
-#= require jquery.atwho
-#= require task_list
-
-class @Notes
- @interval: null
-
- constructor: (notes_url, note_ids, last_fetched_at, view) ->
- @notes_url = notes_url
- @note_ids = note_ids
- @last_fetched_at = last_fetched_at
- @view = view
- @noteable_url = document.URL
- @notesCountBadge ||= $(".issuable-details").find(".notes-tab .badge")
- @basePollingInterval = 15000
- @maxPollingSteps = 4
-
- @cleanBinding()
- @addBinding()
- @setPollingInterval()
- @setupMainTargetNoteForm()
- @initTaskList()
-
- addBinding: ->
- # add note to UI after creation
- $(document).on "ajax:success", ".js-main-target-form", @addNote
- $(document).on "ajax:success", ".js-discussion-note-form", @addDiscussionNote
-
- # catch note ajax errors
- $(document).on "ajax:error", ".js-main-target-form", @addNoteError
-
- # change note in UI after update
- $(document).on "ajax:success", "form.edit-note", @updateNote
-
- # Edit note link
- $(document).on "click", ".js-note-edit", @showEditForm
- $(document).on "click", ".note-edit-cancel", @cancelEdit
-
- # Reopen and close actions for Issue/MR combined with note form submit
- $(document).on "click", ".js-comment-button", @updateCloseButton
- $(document).on "keyup input", ".js-note-text", @updateTargetButtons
-
- # remove a note (in general)
- $(document).on "click", ".js-note-delete", @removeNote
-
- # delete note attachment
- $(document).on "click", ".js-note-attachment-delete", @removeAttachment
-
- # reset main target form after submit
- $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
- $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
-
- # reset main target form when clicking discard
- $(document).on "click", ".js-note-discard", @resetMainTargetForm
-
- # update the file name when an attachment is selected
- $(document).on "change", ".js-note-attachment-input", @updateFormAttachment
-
- # reply to diff/discussion notes
- $(document).on "click", ".js-discussion-reply-button", @replyToDiscussionNote
-
- # add diff note
- $(document).on "click", ".js-add-diff-note-button", @addDiffNote
-
- # hide diff note form
- $(document).on "click", ".js-close-discussion-note-form", @cancelDiscussionForm
-
- # fetch notes when tab becomes visible
- $(document).on "visibilitychange", @visibilityChange
-
- # when issue status changes, we need to refresh data
- $(document).on "issuable:change", @refresh
-
- # when a key is clicked on the notes
- $(document).on "keydown", ".js-note-text", @keydownNoteText
-
- cleanBinding: ->
- $(document).off "ajax:success", ".js-main-target-form"
- $(document).off "ajax:success", ".js-discussion-note-form"
- $(document).off "ajax:success", "form.edit-note"
- $(document).off "click", ".js-note-edit"
- $(document).off "click", ".note-edit-cancel"
- $(document).off "click", ".js-note-delete"
- $(document).off "click", ".js-note-attachment-delete"
- $(document).off "ajax:complete", ".js-main-target-form"
- $(document).off "ajax:success", ".js-main-target-form"
- $(document).off "click", ".js-discussion-reply-button"
- $(document).off "click", ".js-add-diff-note-button"
- $(document).off "visibilitychange"
- $(document).off "keyup", ".js-note-text"
- $(document).off "click", ".js-note-target-reopen"
- $(document).off "click", ".js-note-target-close"
- $(document).off "click", ".js-note-discard"
- $(document).off "keydown", ".js-note-text"
-
- $('.note .js-task-list-container').taskList('disable')
- $(document).off 'tasklist:changed', '.note .js-task-list-container'
-
- keydownNoteText: (e) =>
- return if isMetaKey e
-
- $textarea = $(e.target)
-
- # Edit previous note when UP arrow is hit
- switch e.which
- when 38
- return unless $textarea.val() is ''
-
- myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
- if myLastNote.length
- myLastNoteEditBtn = myLastNote.find('.js-note-edit')
- myLastNoteEditBtn.trigger('click', [true, myLastNote])
-
- # Cancel creating diff note or editing any note when ESCAPE is hit
- when 27
- discussionNoteForm = $textarea.closest('.js-discussion-note-form')
- if discussionNoteForm.length
- if $textarea.val() isnt ''
- return unless confirm('Are you sure you want to cancel creating this comment?')
-
- @removeDiscussionNoteForm(discussionNoteForm)
- return
-
- editNote = $textarea.closest('.note')
- if editNote.length
- originalText = $textarea.closest('form').data('original-note')
- newText = $textarea.val()
- if originalText isnt newText
- return unless confirm('Are you sure you want to cancel editing this comment?')
-
- @removeNoteEditForm(editNote)
-
-
- isMetaKey = (e) ->
- (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
-
- initRefresh: ->
- clearInterval(Notes.interval)
- Notes.interval = setInterval =>
- @refresh()
- , @pollingInterval
-
- refresh: =>
- if not document.hidden and document.URL.indexOf(@noteable_url) is 0
- @getContent()
-
- getContent: ->
- return if @refreshing
-
- @refreshing = true
-
- $.ajax
- url: @notes_url
- data: "last_fetched_at=" + @last_fetched_at
- dataType: "json"
- success: (data) =>
- notes = data.notes
- @last_fetched_at = data.last_fetched_at
- @setPollingInterval(data.notes.length)
- $.each notes, (i, note) =>
- if note.discussion_with_diff_html?
- @renderDiscussionNote(note)
- else
- @renderNote(note)
- .always () =>
- @refreshing = false
-
- ###
- Increase @pollingInterval up to 120 seconds on every function call,
- if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
- will reset to @basePollingInterval.
-
- Note: this function is used to gradually increase the polling interval
- if there aren't new notes coming from the server
- ###
- setPollingInterval: (shouldReset = true) ->
- nthInterval = @basePollingInterval * Math.pow(2, @maxPollingSteps - 1)
- if shouldReset
- @pollingInterval = @basePollingInterval
- else if @pollingInterval < nthInterval
- @pollingInterval *= 2
-
- @initRefresh()
-
- ###
- Render note in main comments area.
-
- Note: for rendering inline notes use renderDiscussionNote
- ###
- renderNote: (note) ->
- unless note.valid
- if note.award
- flash = new Flash('You have already awarded this emoji!', 'alert')
- flash.pinTo('.header-content')
- return
-
- if note.award
- votesBlock = $('.js-awards-block').eq 0
- gl.awardsHandler.addAwardToEmojiBar votesBlock, note.name
- gl.awardsHandler.scrollToAwards()
-
- # render note if it not present in loaded list
- # or skip if rendered
- else if @isNewNote(note)
- @note_ids.push(note.id)
-
- $notesList = $('ul.main-notes-list')
-
- $notesList
- .append(note.html)
- .syntaxHighlight()
-
- # Update datetime format on the recent note
- gl.utils.localTimeAgo($notesList.find("#note_#{note.id} .js-timeago"), false)
-
- @initTaskList()
- @updateNotesCount(1)
-
-
- ###
- Check if note does not exists on page
- ###
- isNewNote: (note) ->
- $.inArray(note.id, @note_ids) == -1
-
- isParallelView: ->
- @view == 'parallel'
-
- ###
- Render note in discussion area.
-
- Note: for rendering inline notes use renderDiscussionNote
- ###
- renderDiscussionNote: (note) ->
- return unless @isNewNote(note)
-
- @note_ids.push(note.id)
- form = $("#new-discussion-note-form-#{note.discussion_id}")
- if note.original_discussion_id? and form.length is 0
- form = $("#new-discussion-note-form-#{note.original_discussion_id}")
- row = form.closest("tr")
- note_html = $(note.html)
- note_html.syntaxHighlight()
-
- # is this the first note of discussion?
- discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
- if note.original_discussion_id? and discussionContainer.length is 0
- discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']")
- if discussionContainer.length is 0
- # insert the note and the reply button after the temp row
- row.after note.discussion_html
-
- # remove the note (will be added again below)
- row.next().find(".note").remove()
-
- # Before that, the container didn't exist
- discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']")
-
- # Add note to 'Changes' page discussions
- discussionContainer.append note_html
-
- # Init discussion on 'Discussion' page if it is merge request page
- if $('body').attr('data-page').indexOf('projects:merge_request') is 0
- $('ul.main-notes-list')
- .append(note.discussion_with_diff_html)
- .syntaxHighlight()
- else
- # append new note to all matching discussions
- discussionContainer.append note_html
-
- gl.utils.localTimeAgo($('.js-timeago', note_html), false)
-
- @updateNotesCount(1)
-
- ###
- Called in response the main target form has been successfully submitted.
-
- Removes any errors.
- Resets text and preview.
- Resets buttons.
- ###
- resetMainTargetForm: (e) =>
- form = $(".js-main-target-form")
-
- # remove validation errors
- form.find(".js-errors").remove()
-
- # reset text and preview
- form.find(".js-md-write-button").click()
- form.find(".js-note-text").val("").trigger "input"
-
- form.find(".js-note-text").data("autosave").reset()
-
- @updateTargetButtons(e)
-
- reenableTargetFormSubmitButton: ->
- form = $(".js-main-target-form")
-
- form.find(".js-note-text").trigger "input"
-
- ###
- Shows the main form and does some setup on it.
-
- Sets some hidden fields in the form.
- ###
- setupMainTargetNoteForm: ->
- # find the form
- form = $(".js-new-note-form")
-
- # Set a global clone of the form for later cloning
- @formClone = form.clone()
-
- # show the form
- @setupNoteForm(form)
-
- # fix classes
- form.removeClass "js-new-note-form"
- form.addClass "js-main-target-form"
-
- form.find("#note_line_code").remove()
- form.find("#note_position").remove()
- form.find("#note_type").remove()
-
- ###
- General note form setup.
-
- deactivates the submit button when text is empty
- hides the preview button when text is empty
- setup GFM auto complete
- show the form
- ###
- setupNoteForm: (form) ->
- new GLForm form
-
- textarea = form.find(".js-note-text")
-
- new Autosave textarea, [
- "Note"
- form.find("#note_noteable_type").val()
- form.find("#note_noteable_id").val()
- form.find("#note_commit_id").val()
- form.find("#note_type").val()
- form.find("#note_line_code").val()
- form.find("#note_position").val()
- ]
-
- ###
- Called in response to the new note form being submitted
-
- Adds new note to list.
- ###
- addNote: (xhr, note, status) =>
- @renderNote(note)
-
- addNoteError: (xhr, note, status) =>
- flash = new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert')
- flash.pinTo('.md-area')
-
- ###
- Called in response to the new note form being submitted
-
- Adds new note to list.
- ###
- addDiscussionNote: (xhr, note, status) =>
- @renderDiscussionNote(note)
-
- # cleanup after successfully creating a diff/discussion note
- @removeDiscussionNoteForm($(xhr.target))
-
- ###
- Called in response to the edit note form being submitted
-
- Updates the current note field.
- ###
- updateNote: (_xhr, note, _status) =>
- # 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.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)
-
- ###
- Called in response to clicking the edit note link
-
- Replaces the note text with the note edit form
- Adds a data attribute to the form with the original content of the note for cancellations
- ###
- showEditForm: (e, scrollTo, myLastNote) ->
- e.preventDefault()
- note = $(this).closest(".note")
- note.addClass "is-editting"
- form = note.find(".note-edit-form")
-
- form.addClass('current-note-edit-form')
-
- # Show the attachment delete link
- note.find(".js-note-attachment-delete").show()
-
- done = ($noteText) ->
- # Neat little trick to put the cursor at the end
- noteTextVal = $noteText.val()
- # Store the original note text in a data attribute to retrieve if a user cancels edit.
- form.find('form.edit-note').data 'original-note', noteTextVal
- $noteText.val('').val(noteTextVal);
-
- new GLForm form
- if scrollTo? and myLastNote?
- # scroll to the bottom
- # so the open of the last element doesn't make a jump
- $('html, body').scrollTop($(document).height());
- $('html, body').animate({
- scrollTop: myLastNote.offset().top - 150
- }, 500, ->
- $noteText = form.find(".js-note-text")
- $noteText.focus()
- done($noteText)
- );
- else
- $noteText = form.find('.js-note-text')
- $noteText.focus()
- done($noteText)
-
- ###
- Called in response to clicking the edit note link
-
- Hides edit form and restores the original note text to the editor textarea.
- ###
- cancelEdit: (e) =>
- e.preventDefault()
- note = $(e.target).closest('.note')
- @removeNoteEditForm(note)
-
- removeNoteEditForm: (note) ->
- form = note.find(".current-note-edit-form")
- note.removeClass "is-editting"
- form.removeClass("current-note-edit-form")
- # Replace markdown textarea text with original note text.
- form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'))
-
- ###
- Called in response to deleting a note of any kind.
-
- Removes the actual note from view.
- Removes the whole discussion if the last note is being removed.
- ###
- removeNote: (e) =>
- noteId = $(e.currentTarget)
- .closest(".note")
- .attr("id")
-
- # A same note appears in the "Discussion" and in the "Changes" tab, we have
- # to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
- # where $("#noteId") would return only one.
- $(".note[id='#{noteId}']").each (i, el) =>
- note = $(el)
- notes = note.closest(".notes")
-
- # check if this is the last note for this line
- if notes.find(".note").length is 1
-
- # "Discussions" tab
- notes.closest(".timeline-entry").remove()
-
- # "Changes" tab / commit view
- notes.closest("tr").remove()
-
- note.remove()
-
- # Decrement the "Discussions" counter only once
- @updateNotesCount(-1)
-
- ###
- Called in response to clicking the delete attachment link
-
- Removes the attachment wrapper view, including image tag if it exists
- Resets the note editing form
- ###
- removeAttachment: ->
- note = $(this).closest(".note")
- note.find(".note-attachment").remove()
- note.find(".note-body > .note-text").show()
- note.find(".note-header").show()
- note.find(".current-note-edit-form").remove()
-
- ###
- Called when clicking on the "reply" button for a diff line.
-
- Shows the note form below the notes.
- ###
- replyToDiscussionNote: (e) =>
- form = @formClone.clone()
- replyLink = $(e.target).closest(".js-discussion-reply-button")
- replyLink.hide()
-
- # insert the form after the button
- replyLink.after form
-
- # show the form
- @setupDiscussionNoteForm(replyLink, form)
-
- ###
- Shows the diff or discussion form and does some setup on it.
-
- Sets some hidden fields in the form.
-
- Note: dataHolder must have the "discussionId", "lineCode", "noteableType"
- and "noteableId" data attributes set.
- ###
- setupDiscussionNoteForm: (dataHolder, form) =>
- # setup note target
- form.attr 'id', "new-discussion-note-form-#{dataHolder.data("discussionId")}"
- form.attr "data-line-code", dataHolder.data("lineCode")
- form.find("#note_type").val dataHolder.data("noteType")
- form.find("#line_type").val dataHolder.data("lineType")
- form.find("#note_commit_id").val dataHolder.data("commitId")
- form.find("#note_line_code").val dataHolder.data("lineCode")
- form.find("#note_position").val dataHolder.attr("data-position")
- form.find("#note_noteable_type").val dataHolder.data("noteableType")
- form.find("#note_noteable_id").val dataHolder.data("noteableId")
- form.find('.js-note-discard')
- .show()
- .removeClass('js-note-discard')
- .addClass('js-close-discussion-note-form')
- .text(form.find('.js-close-discussion-note-form').data('cancel-text'))
- @setupNoteForm form
- form.find(".js-note-text").focus()
- form
- .removeClass('js-main-target-form')
- .addClass("discussion-form js-discussion-note-form")
-
- ###
- Called when clicking on the "add a comment" button on the side of a diff line.
-
- Inserts a temporary row for the form below the line.
- Sets up the form and shows it.
- ###
- addDiffNote: (e) =>
- e.preventDefault()
- $link = $(e.currentTarget)
- row = $link.closest("tr")
- nextRow = row.next()
- hasNotes = nextRow.is(".notes_holder")
- addForm = false
- targetContent = ".notes_content"
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"></td></tr>"
-
- # In parallel view, look inside the correct left/right pane
- if @isParallelView()
- lineType = $link.data("lineType")
- targetContent += "." + lineType
- rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\"></td><td class=\"notes_content parallel old\"></td><td class=\"notes_line\"></td><td class=\"notes_content parallel new\"></td></tr>"
-
- if hasNotes
- notesContent = nextRow.find(targetContent)
- if notesContent.length
- replyButton = notesContent.find(".js-discussion-reply-button:visible")
- if replyButton.length
- e.target = replyButton[0]
- $.proxy(@replyToDiscussionNote, replyButton[0], e).call()
- else
- # In parallel view, the form may not be present in one of the panes
- noteForm = notesContent.find(".js-discussion-note-form")
- if noteForm.length == 0
- addForm = true
- else
- # add a notes row and insert the form
- row.after rowCssToAdd
- addForm = true
-
- if addForm
- newForm = @formClone.clone()
- newForm.appendTo row.next().find(targetContent)
-
- # show the form
- @setupDiscussionNoteForm $link, newForm
-
- ###
- Called in response to "cancel" on a diff note form.
-
- Shows the reply button again.
- Removes the form and if necessary it's temporary row.
- ###
- removeDiscussionNoteForm: (form)->
- row = form.closest("tr")
-
- glForm = form.data 'gl-form'
- glForm.destroy()
-
- form.find(".js-note-text").data("autosave").reset()
-
- # show the reply button (will only work for replies)
- form.prev(".js-discussion-reply-button").show()
- if row.is(".js-temp-notes-holder")
- # remove temporary row for diff lines
- row.remove()
- else
- # only remove the form
- form.remove()
-
- cancelDiscussionForm: (e) =>
- e.preventDefault()
- form = $(e.target).closest(".js-discussion-note-form")
- @removeDiscussionNoteForm(form)
-
- ###
- Called after an attachment file has been selected.
-
- Updates the file name for the selected attachment.
- ###
- updateFormAttachment: ->
- form = $(this).closest("form")
-
- # get only the basename
- filename = $(this).val().replace(/^.*[\\\/]/, "")
- form.find(".js-attachment-filename").text filename
-
- ###
- Called when the tab visibility changes
- ###
- visibilityChange: =>
- @refresh()
-
- updateCloseButton: (e) =>
- textarea = $(e.target)
- form = textarea.parents('form')
- closebtn = form.find('.js-note-target-close')
- closebtn.text(closebtn.data('original-text'))
-
- updateTargetButtons: (e) =>
- textarea = $(e.target)
- form = textarea.parents('form')
- reopenbtn = form.find('.js-note-target-reopen')
- closebtn = form.find('.js-note-target-close')
- discardbtn = form.find('.js-note-discard')
-
- if textarea.val().trim().length > 0
- reopentext = reopenbtn.data('alternative-text')
- closetext = closebtn.data('alternative-text')
-
- if reopenbtn.text() isnt reopentext
- reopenbtn.text(reopentext)
-
- if closebtn.text() isnt closetext
- closebtn.text(closetext)
-
- if reopenbtn.is(':not(.btn-comment-and-reopen)')
- reopenbtn.addClass('btn-comment-and-reopen')
-
- if closebtn.is(':not(.btn-comment-and-close)')
- closebtn.addClass('btn-comment-and-close')
-
- if discardbtn.is(':hidden')
- discardbtn.show()
- else
- reopentext = reopenbtn.data('original-text')
- closetext = closebtn.data('original-text')
-
- if reopenbtn.text() isnt reopentext
- reopenbtn.text(reopentext)
-
- if closebtn.text() isnt closetext
- closebtn.text(closetext)
-
- if reopenbtn.is('.btn-comment-and-reopen')
- reopenbtn.removeClass('btn-comment-and-reopen')
-
- if closebtn.is('.btn-comment-and-close')
- closebtn.removeClass('btn-comment-and-close')
-
- if discardbtn.is(':visible')
- discardbtn.hide()
-
- initTaskList: ->
- @enableTaskList()
- $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList
-
- enableTaskList: ->
- $('.note .js-task-list-container').taskList('enable')
-
- updateTaskList: ->
- $('form', this).submit()
-
- updateNotesCount: (updateCount) ->
- @notesCountBadge.text(parseInt(@notesCountBadge.text()) + updateCount)
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
new file mode 100644
index 00000000000..a41e9d3fabe
--- /dev/null
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -0,0 +1,30 @@
+(function() {
+ this.NotificationsDropdown = (function() {
+ function NotificationsDropdown() {
+ $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) {
+ var form, label, notificationLevel;
+ e.preventDefault();
+ if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') {
+ return;
+ }
+ notificationLevel = $(this).data('notification-level');
+ label = $(this).data('notification-title');
+ form = $(this).parents('.notification-form:first');
+ form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
+ form.find('#notification_setting_level').val(notificationLevel);
+ return form.submit();
+ });
+ $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) {
+ if (data.saved) {
+ return $(e.currentTarget).closest('.notification-dropdown').replaceWith(data.html);
+ } else {
+ return new Flash('Failed to save new settings', 'alert');
+ }
+ });
+ }
+
+ return NotificationsDropdown;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee
deleted file mode 100644
index 0bbd082c156..00000000000
--- a/app/assets/javascripts/notifications_dropdown.js.coffee
+++ /dev/null
@@ -1,25 +0,0 @@
-class @NotificationsDropdown
- constructor: ->
- $(document)
- .off 'click', '.update-notification'
- .on 'click', '.update-notification', (e) ->
- e.preventDefault()
-
- return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
-
- notificationLevel = $(@).data 'notification-level'
- label = $(@).data 'notification-title'
- form = $(this).parents('.notification-form:first')
- form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
- form.find('#notification_setting_level').val(notificationLevel)
- form.submit()
-
- $(document)
- .off 'ajax:success', '.notification-form'
- .on 'ajax:success', '.notification-form', (e, data) ->
- if data.saved
- $(e.currentTarget)
- .closest('.notification-dropdown')
- .replaceWith(data.html)
- else
- new Flash('Failed to save new settings', 'alert')
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
new file mode 100644
index 00000000000..6b2ef17ef6b
--- /dev/null
+++ b/app/assets/javascripts/notifications_form.js
@@ -0,0 +1,58 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.NotificationsForm = (function() {
+ function NotificationsForm() {
+ this.toggleCheckbox = bind(this.toggleCheckbox, this);
+ this.removeEventListeners();
+ this.initEventListeners();
+ }
+
+ NotificationsForm.prototype.removeEventListeners = function() {
+ return $(document).off('change', '.js-custom-notification-event');
+ };
+
+ NotificationsForm.prototype.initEventListeners = function() {
+ return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox);
+ };
+
+ NotificationsForm.prototype.toggleCheckbox = function(e) {
+ var $checkbox, $parent;
+ $checkbox = $(e.currentTarget);
+ $parent = $checkbox.closest('.checkbox');
+ return this.saveEvent($checkbox, $parent);
+ };
+
+ NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) {
+ return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done');
+ };
+
+ NotificationsForm.prototype.saveEvent = function($checkbox, $parent) {
+ var form;
+ form = $parent.parents('form:first');
+ return $.ajax({
+ url: form.attr('action'),
+ method: form.attr('method'),
+ dataType: 'json',
+ data: form.serialize(),
+ beforeSend: (function(_this) {
+ return function() {
+ return _this.showCheckboxLoadingSpinner($parent);
+ };
+ })(this)
+ }).done(function(data) {
+ $checkbox.enable();
+ if (data.saved) {
+ $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
+ return setTimeout(function() {
+ return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done');
+ }, 2000);
+ }
+ });
+ };
+
+ return NotificationsForm;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee
deleted file mode 100644
index 3432428702a..00000000000
--- a/app/assets/javascripts/notifications_form.js.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-class @NotificationsForm
- constructor: ->
- @removeEventListeners()
- @initEventListeners()
-
- removeEventListeners: ->
- $(document).off 'change', '.js-custom-notification-event'
-
- initEventListeners: ->
- $(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
-
- toggleCheckbox: (e) =>
- $checkbox = $(e.currentTarget)
- $parent = $checkbox.closest('.checkbox')
- @saveEvent($checkbox, $parent)
-
- showCheckboxLoadingSpinner: ($parent) ->
- $parent
- .addClass 'is-loading'
- .find '.custom-notification-event-loading'
- .removeClass 'fa-check'
- .addClass 'fa-spin fa-spinner'
- .removeClass 'is-done'
-
- saveEvent: ($checkbox, $parent) ->
- form = $parent.parents('form:first')
-
- $.ajax(
- url: form.attr('action')
- method: form.attr('method')
- dataType: 'json'
- data: form.serialize()
-
- beforeSend: =>
- @showCheckboxLoadingSpinner($parent)
- ).done (data) ->
- $checkbox.enable()
-
- if data.saved
- $parent
- .find '.custom-notification-event-loading'
- .toggleClass 'fa-spin fa-spinner fa-check is-done'
-
- setTimeout(->
- $parent
- .removeClass 'is-loading'
- .find '.custom-notification-event-loading'
- .toggleClass 'fa-spin fa-spinner fa-check is-done'
- , 2000)
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
new file mode 100644
index 00000000000..b81ed50cb48
--- /dev/null
+++ b/app/assets/javascripts/pager.js
@@ -0,0 +1,63 @@
+(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.coffee b/app/assets/javascripts/pager.js.coffee
deleted file mode 100644
index 8049c5c30e2..00000000000
--- a/app/assets/javascripts/pager.js.coffee
+++ /dev/null
@@ -1,44 +0,0 @@
-@Pager =
- init: (@limit = 0, preload, @disable = false, @callback = $.noop) ->
- @loading = $('.loading').first()
-
- if preload
- @offset = 0
- @getOld()
- else
- @offset = @limit
- @initLoadMore()
-
- getOld: ->
- @loading.show()
- $.ajax
- type: "GET"
- url: $(".content_list").data('href') || location.href
- data: "limit=" + @limit + "&offset=" + @offset
- complete: =>
- @loading.hide()
- success: (data) ->
- Pager.append(data.count, data.html)
- Pager.callback()
- dataType: "json"
-
- append: (count, html) ->
- $(".content_list").append html
- if count > 0
- @offset += count
- else
- @disable = true
-
- initLoadMore: ->
- $(document).unbind('scroll')
- $(document).endlessScroll
- bottomPixels: 400
- fireDelay: 1000
- fireOnce: true
- ceaseFire: ->
- Pager.disable
-
- callback: (i) =>
- unless @loading.is(':visible')
- @loading.show()
- Pager.getOld()
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
new file mode 100644
index 00000000000..bf33eb10100
--- /dev/null
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -0,0 +1,15 @@
+(function() {
+ function toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+
+ graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ }
+
+ $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
+})();
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
new file mode 100644
index 00000000000..5fd75799640
--- /dev/null
+++ b/app/assets/javascripts/preview_markdown.js
@@ -0,0 +1,150 @@
+(function() {
+ var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
+
+ this.MarkdownPreview = (function() {
+ function MarkdownPreview() {}
+
+ MarkdownPreview.prototype.referenceThreshold = 10;
+
+ MarkdownPreview.prototype.ajaxCache = {};
+
+ MarkdownPreview.prototype.showPreview = function(form) {
+ var mdText, preview;
+ preview = form.find('.js-md-preview');
+ mdText = form.find('textarea.markdown-area').val();
+ if (mdText.trim().length === 0) {
+ preview.text('Nothing to preview.');
+ return this.hideReferencedUsers(form);
+ } else {
+ preview.text('Loading...');
+ return this.renderMarkdown(mdText, (function(_this) {
+ return function(response) {
+ preview.html(response.body);
+ preview.syntaxHighlight();
+ return _this.renderReferencedUsers(response.references.users, form);
+ };
+ })(this));
+ }
+ };
+
+ MarkdownPreview.prototype.renderMarkdown = function(text, success) {
+ if (!window.preview_markdown_path) {
+ return;
+ }
+ if (text === this.ajaxCache.text) {
+ return success(this.ajaxCache.response);
+ }
+ return $.ajax({
+ type: 'POST',
+ url: window.preview_markdown_path,
+ data: {
+ text: text
+ },
+ dataType: 'json',
+ success: (function(_this) {
+ return function(response) {
+ _this.ajaxCache = {
+ text: text,
+ response: response
+ };
+ return success(response);
+ };
+ })(this)
+ });
+ };
+
+ MarkdownPreview.prototype.hideReferencedUsers = function(form) {
+ var referencedUsers;
+ referencedUsers = form.find('.referenced-users');
+ return referencedUsers.hide();
+ };
+
+ MarkdownPreview.prototype.renderReferencedUsers = function(users, form) {
+ var referencedUsers;
+ referencedUsers = form.find('.referenced-users');
+ if (referencedUsers.length) {
+ if (users.length >= this.referenceThreshold) {
+ referencedUsers.show();
+ return referencedUsers.find('.js-referenced-users-count').text(users.length);
+ } else {
+ return referencedUsers.hide();
+ }
+ }
+ };
+
+ return MarkdownPreview;
+
+ })();
+
+ markdownPreview = new MarkdownPreview();
+
+ previewButtonSelector = '.js-md-preview-button';
+
+ writeButtonSelector = '.js-md-write-button';
+
+ lastTextareaPreviewed = null;
+
+ $.fn.setupMarkdownPreview = function() {
+ var $form, form_textarea;
+ $form = $(this);
+ form_textarea = $form.find('textarea.markdown-area');
+ form_textarea.on('input', function() {
+ return markdownPreview.hideReferencedUsers($form);
+ });
+ return form_textarea.on('blur', function() {
+ return markdownPreview.showPreview($form);
+ });
+ };
+
+ $(document).on('markdown-preview:show', function(e, $form) {
+ if (!$form) {
+ return;
+ }
+ lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ $form.find(writeButtonSelector).parent().removeClass('active');
+ $form.find(previewButtonSelector).parent().addClass('active');
+ $form.find('.md-write-holder').hide();
+ $form.find('.md-preview-holder').show();
+ return markdownPreview.showPreview($form);
+ });
+
+ $(document).on('markdown-preview:hide', function(e, $form) {
+ if (!$form) {
+ return;
+ }
+ lastTextareaPreviewed = null;
+ $form.find(writeButtonSelector).parent().addClass('active');
+ $form.find(previewButtonSelector).parent().removeClass('active');
+ $form.find('.md-write-holder').show();
+ $form.find('textarea.markdown-area').focus();
+ return $form.find('.md-preview-holder').hide();
+ });
+
+ $(document).on('markdown-preview:toggle', function(e, keyboardEvent) {
+ var $target;
+ $target = $(keyboardEvent.target);
+ if ($target.is('textarea.markdown-area')) {
+ $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]);
+ return keyboardEvent.preventDefault();
+ } else if (lastTextareaPreviewed) {
+ $target = lastTextareaPreviewed;
+ $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]);
+ return keyboardEvent.preventDefault();
+ }
+ });
+
+ $(document).on('click', previewButtonSelector, function(e) {
+ var $form;
+ e.preventDefault();
+ $form = $(this).closest('form');
+ return $(document).triggerHandler('markdown-preview:show', [$form]);
+ });
+
+ $(document).on('click', writeButtonSelector, function(e) {
+ var $form;
+ e.preventDefault();
+ $form = $(this).closest('form');
+ return $(document).triggerHandler('markdown-preview:hide', [$form]);
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/profile/application.js.coffee b/app/assets/javascripts/profile/application.js.coffee
deleted file mode 100644
index 91cacfece46..00000000000
--- a/app/assets/javascripts/profile/application.js.coffee
+++ /dev/null
@@ -1,2 +0,0 @@
-#
-#= require_tree .
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
new file mode 100644
index 00000000000..a3eea316f67
--- /dev/null
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -0,0 +1,169 @@
+(function() {
+ var GitLabCrop,
+ bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ GitLabCrop = (function() {
+ var FILENAMEREGEX;
+
+ FILENAMEREGEX = /^.*[\\\/]/;
+
+ function GitLabCrop(input, opts) {
+ var ref, ref1, ref2, ref3, ref4;
+ if (opts == null) {
+ opts = {};
+ }
+ this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
+ this.onModalHide = bind(this.onModalHide, this);
+ this.onModalShow = bind(this.onModalShow, this);
+ this.onPickImageClick = bind(this.onPickImageClick, this);
+ this.fileInput = $(input);
+ this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
+ this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
+ this.filename = this.getElement(this.filename);
+ this.previewImage = this.getElement(this.previewImage);
+ this.pickImageEl = this.getElement(this.pickImageEl);
+ this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
+ this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
+ this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
+ this.cropActionsBtn = this.modalCrop.find('[data-method]');
+ this.bindEvents();
+ }
+
+ GitLabCrop.prototype.getElement = function(selector) {
+ return $(selector, this.form);
+ };
+
+ GitLabCrop.prototype.bindEvents = function() {
+ var _this;
+ _this = this;
+ this.fileInput.on('change', function(e) {
+ return _this.onFileInputChange(e, this);
+ });
+ this.pickImageEl.on('click', this.onPickImageClick);
+ this.modalCrop.on('shown.bs.modal', this.onModalShow);
+ this.modalCrop.on('hidden.bs.modal', this.onModalHide);
+ this.uploadImageBtn.on('click', this.onUploadImageBtnClick);
+ this.cropActionsBtn.on('click', function(e) {
+ var btn;
+ btn = this;
+ return _this.onActionBtnClick(btn);
+ });
+ return this.croppedImageBlob = null;
+ };
+
+ GitLabCrop.prototype.onPickImageClick = function() {
+ return this.fileInput.trigger('click');
+ };
+
+ GitLabCrop.prototype.onModalShow = function() {
+ var _this;
+ _this = this;
+ return this.modalCropImg.cropper({
+ viewMode: 1,
+ center: false,
+ aspectRatio: 1,
+ modal: true,
+ scalable: false,
+ rotatable: false,
+ zoomable: true,
+ dragMode: 'move',
+ guides: false,
+ zoomOnTouch: false,
+ zoomOnWheel: false,
+ cropBoxMovable: false,
+ cropBoxResizable: false,
+ toggleDragModeOnDblclick: false,
+ built: function() {
+ var $image, container, cropBoxHeight, cropBoxWidth;
+ $image = $(this);
+ container = $image.cropper('getContainerData');
+ cropBoxWidth = _this.cropBoxWidth;
+ cropBoxHeight = _this.cropBoxHeight;
+ return $image.cropper('setCropBoxData', {
+ width: cropBoxWidth,
+ height: cropBoxHeight,
+ left: (container.width - cropBoxWidth) / 2,
+ top: (container.height - cropBoxHeight) / 2
+ });
+ }
+ });
+ };
+
+ GitLabCrop.prototype.onModalHide = function() {
+ return this.modalCropImg.attr('src', '').cropper('destroy');
+ };
+
+ GitLabCrop.prototype.onUploadImageBtnClick = function(e) {
+ e.preventDefault();
+ this.setBlob();
+ this.setPreview();
+ this.modalCrop.modal('hide');
+ return this.fileInput.val('');
+ };
+
+ GitLabCrop.prototype.onActionBtnClick = function(btn) {
+ var data, result;
+ data = $(btn).data();
+ if (this.modalCropImg.data('cropper') && data.method) {
+ return result = this.modalCropImg.cropper(data.method, data.option);
+ }
+ };
+
+ GitLabCrop.prototype.onFileInputChange = function(e, input) {
+ return this.readFile(input);
+ };
+
+ GitLabCrop.prototype.readFile = function(input) {
+ var _this, reader;
+ _this = this;
+ reader = new FileReader;
+ reader.onload = function() {
+ _this.modalCropImg.attr('src', reader.result);
+ return _this.modalCrop.modal('show');
+ };
+ return reader.readAsDataURL(input.files[0]);
+ };
+
+ GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
+ var array, binary, i, k, len, v;
+ binary = atob(dataURL.split(',')[1]);
+ array = [];
+ for (k = i = 0, len = binary.length; i < len; k = ++i) {
+ v = binary[k];
+ array.push(binary.charCodeAt(k));
+ }
+ return new Blob([new Uint8Array(array)], {
+ type: 'image/png'
+ });
+ };
+
+ GitLabCrop.prototype.setPreview = function() {
+ var filename;
+ this.previewImage.attr('src', this.dataURL);
+ filename = this.fileInput.val().replace(FILENAMEREGEX, '');
+ return this.filename.text(filename);
+ };
+
+ GitLabCrop.prototype.setBlob = function() {
+ this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
+ width: 200,
+ height: 200
+ }).toDataURL('image/png');
+ return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
+ };
+
+ GitLabCrop.prototype.getBlob = function() {
+ return this.croppedImageBlob;
+ };
+
+ return GitLabCrop;
+
+ })();
+
+ $.fn.glCrop = function(opts) {
+ return this.each(function() {
+ return $(this).data('glcrop', new GitLabCrop(this, opts));
+ });
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/profile/gl_crop.js.coffee b/app/assets/javascripts/profile/gl_crop.js.coffee
deleted file mode 100644
index df9bfdfa6cc..00000000000
--- a/app/assets/javascripts/profile/gl_crop.js.coffee
+++ /dev/null
@@ -1,152 +0,0 @@
-class GitLabCrop
- # Matches everything but the file name
- FILENAMEREGEX = /^.*[\\\/]/
-
- constructor: (input, opts = {}) ->
- @fileInput = $(input)
-
- # We should rename to avoid spec to fail
- # Form will submit the proper input filed with a file using FormData
- @fileInput
- .attr('name', "#{@fileInput.attr('name')}-trigger")
- .attr('id', "#{@fileInput.attr('id')}-trigger")
-
- # Set defaults
- {
- @exportWidth = 200
- @exportHeight = 200
- @cropBoxWidth = 200
- @cropBoxHeight = 200
- @form = @fileInput.parents('form')
-
- # Required params
- @filename
- @previewImage
- @modalCrop
- @pickImageEl
- @uploadImageBtn
- @modalCropImg
- } = opts
-
- # Ensure needed elements are jquery objects
- # If selector is provided we will convert them to a jQuery Object
- @filename = @getElement(@filename)
- @previewImage = @getElement(@previewImage)
- @pickImageEl = @getElement(@pickImageEl)
-
- # Modal elements usually are outside the @form element
- @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
- @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
- @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
-
- @cropActionsBtn = @modalCrop.find('[data-method]')
-
- @bindEvents()
-
- getElement: (selector) ->
- $(selector, @form)
-
- bindEvents: ->
- _this = @
- @fileInput.on 'change', (e) ->
- _this.onFileInputChange(e, @)
-
- @pickImageEl.on 'click', @onPickImageClick
- @modalCrop.on 'shown.bs.modal', @onModalShow
- @modalCrop.on 'hidden.bs.modal', @onModalHide
- @uploadImageBtn.on 'click', @onUploadImageBtnClick
- @cropActionsBtn.on 'click', (e) ->
- btn = @
- _this.onActionBtnClick(btn)
- @croppedImageBlob = null
-
- onPickImageClick: =>
- @fileInput.trigger('click')
-
- onModalShow: =>
- _this = @
- @modalCropImg.cropper(
- viewMode: 1
- center: false
- aspectRatio: 1
- modal: true
- scalable: false
- rotatable: false
- zoomable: true
- dragMode: 'move'
- guides: false
- zoomOnTouch: false
- zoomOnWheel: false
- cropBoxMovable: false
- cropBoxResizable: false
- toggleDragModeOnDblclick: false
- built: ->
- $image = $(@)
- container = $image.cropper 'getContainerData'
- cropBoxWidth = _this.cropBoxWidth;
- cropBoxHeight = _this.cropBoxHeight;
-
- $image.cropper('setCropBoxData',
- width: cropBoxWidth,
- height: cropBoxHeight,
- left: (container.width - cropBoxWidth) / 2,
- top: (container.height - cropBoxHeight) / 2
- )
- )
-
-
- onModalHide: =>
- @modalCropImg
- .attr('src', '') # Remove attached image
- .cropper('destroy') # Destroy cropper instance
-
- onUploadImageBtnClick: (e) =>
- e.preventDefault()
- @setBlob()
- @setPreview()
- @modalCrop.modal('hide')
- @fileInput.val('')
-
- onActionBtnClick: (btn) ->
- data = $(btn).data()
-
- if @modalCropImg.data('cropper') && data.method
- result = @modalCropImg.cropper data.method, data.option
-
- onFileInputChange: (e, input) ->
- @readFile(input)
-
- readFile: (input) ->
- _this = @
- reader = new FileReader
- reader.onload = ->
- _this.modalCropImg.attr('src', reader.result)
- _this.modalCrop.modal('show')
-
- reader.readAsDataURL(input.files[0])
-
- dataURLtoBlob: (dataURL) ->
- binary = atob(dataURL.split(',')[1])
- array = []
- for v, k in binary
- array.push(binary.charCodeAt(k))
- new Blob([new Uint8Array(array)], type: 'image/png')
-
- setPreview: ->
- @previewImage.attr('src', @dataURL)
- filename = @fileInput.val().replace(FILENAMEREGEX, '')
- @filename.text(filename)
-
- setBlob: ->
- @dataURL = @modalCropImg.cropper('getCroppedCanvas',
- width: 200
- height: 200
- ).toDataURL('image/png')
- @croppedImageBlob = @dataURLtoBlob(@dataURL)
-
- getBlob: ->
- @croppedImageBlob
-
-$.fn.glCrop = (opts) ->
- return @.each ->
- $(@).data('glcrop', new GitLabCrop(@, opts))
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
new file mode 100644
index 00000000000..ed1d87abafe
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js
@@ -0,0 +1,102 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Profile = (function() {
+ function Profile(opts) {
+ var cropOpts, ref;
+ if (opts == null) {
+ opts = {};
+ }
+ this.onSubmitForm = bind(this.onSubmitForm, this);
+ this.form = (ref = opts.form) != null ? ref : $('.edit-user');
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
+ return $(this).parents('form').submit();
+ });
+ $('#user_notification_email').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.update-username').on('ajax:before', function() {
+ $('.loading-username').show();
+ $(this).find('.update-success').hide();
+ return $(this).find('.update-failed').hide();
+ });
+ $('.update-username').on('ajax:complete', function() {
+ $('.loading-username').hide();
+ $(this).find('.btn-save').enable();
+ return $(this).find('.loading-gif').hide();
+ });
+ $('.update-notifications').on('ajax:success', function(e, data) {
+ if (data.saved) {
+ return new Flash("Notification settings saved", "notice");
+ } else {
+ return new Flash("Failed to save new settings", "alert");
+ }
+ });
+ this.bindEvents();
+ cropOpts = {
+ filename: '.js-avatar-filename',
+ previewImage: '.avatar-image .avatar',
+ modalCrop: '.modal-profile-crop',
+ pickImageEl: '.js-choose-user-avatar-button',
+ uploadImageBtn: '.js-upload-user-avatar',
+ modalCropImg: '.modal-profile-crop-image'
+ };
+ this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ }
+
+ Profile.prototype.bindEvents = function() {
+ return this.form.on('submit', this.onSubmitForm);
+ };
+
+ Profile.prototype.onSubmitForm = function(e) {
+ e.preventDefault();
+ return this.saveForm();
+ };
+
+ Profile.prototype.saveForm = function() {
+ var avatarBlob, formData, self;
+ self = this;
+ formData = new FormData(this.form[0]);
+ avatarBlob = this.avatarGlCrop.getBlob();
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
+ }
+ return $.ajax({
+ url: this.form.attr('action'),
+ type: this.form.attr('method'),
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ success: function(response) {
+ return new Flash(response.message, 'notice');
+ },
+ error: function(jqXHR) {
+ return new Flash(jqXHR.responseJSON.message, 'alert');
+ },
+ complete: function() {
+ window.scrollTo(0, 0);
+ return self.form.find(':input[disabled]').enable();
+ }
+ });
+ };
+
+ return Profile;
+
+ })();
+
+ $(function() {
+ $(document).on('focusout.ssh_key', '#key_key', function() {
+ var $title, comment;
+ $title = $('#key_title');
+ comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+ if (comment && comment.length > 1 && $title.val() === '') {
+ return $title.val(comment[1]).change();
+ }
+ });
+ if (gl.utils.getPagePath() === 'profiles') {
+ return new Profile();
+ }
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/profile/profile.js.coffee b/app/assets/javascripts/profile/profile.js.coffee
deleted file mode 100644
index f3b05f2c646..00000000000
--- a/app/assets/javascripts/profile/profile.js.coffee
+++ /dev/null
@@ -1,83 +0,0 @@
-class @Profile
- constructor: (opts = {}) ->
- {
- @form = $('.edit-user')
- } = opts
-
- # Automatically submit the Preferences form when any of its radio buttons change
- $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
- $(this).parents('form').submit()
-
- # Automatically submit email form when it changes
- $('#user_notification_email').on 'change', ->
- $(this).parents('form').submit()
-
- $('.update-username').on 'ajax:before', ->
- $('.loading-username').show()
- $(this).find('.update-success').hide()
- $(this).find('.update-failed').hide()
-
- $('.update-username').on 'ajax:complete', ->
- $('.loading-username').hide()
- $(this).find('.btn-save').enable()
- $(this).find('.loading-gif').hide()
-
- $('.update-notifications').on 'ajax:success', (e, data) ->
- if data.saved
- new Flash("Notification settings saved", "notice")
- else
- new Flash("Failed to save new settings", "alert")
-
- @bindEvents()
-
- cropOpts =
- filename: '.js-avatar-filename'
- previewImage: '.avatar-image .avatar'
- modalCrop: '.modal-profile-crop'
- pickImageEl: '.js-choose-user-avatar-button'
- uploadImageBtn: '.js-upload-user-avatar'
- modalCropImg: '.modal-profile-crop-image'
-
- @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
-
- bindEvents: ->
- @form.on 'submit', @onSubmitForm
-
- onSubmitForm: (e) =>
- e.preventDefault()
- @saveForm()
-
- saveForm: ->
- self = @
- formData = new FormData(@form[0])
-
- avatarBlob = @avatarGlCrop.getBlob()
- formData.append('user[avatar]', avatarBlob, 'avatar.png') if avatarBlob?
-
- $.ajax
- url: @form.attr('action')
- type: @form.attr('method')
- data: formData
- dataType: "json"
- processData: false
- contentType: false
- success: (response) ->
- new Flash(response.message, 'notice')
- error: (jqXHR) ->
- new Flash(jqXHR.responseJSON.message, 'alert')
- complete: ->
- window.scrollTo 0, 0
- # Enable submit button after requests ends
- self.form.find(':input[disabled]').enable()
-
-$ ->
- # Extract the SSH Key title from its comment
- $(document).on 'focusout.ssh_key', '#key_key', ->
- $title = $('#key_title')
- comment = $(@).val().match(/^\S+ \S+ (.+)\n?$/)
-
- if comment && comment.length > 1 && $title.val() == ''
- $title.val(comment[1]).change()
-
- if gl.utils.getPagePath() == 'profiles'
- new Profile()
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
new file mode 100644
index 00000000000..b95faadc8e7
--- /dev/null
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -0,0 +1,7 @@
+
+/*= require_tree . */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
new file mode 100644
index 00000000000..66e097c0a28
--- /dev/null
+++ b/app/assets/javascripts/project.js
@@ -0,0 +1,106 @@
+(function() {
+ this.Project = (function() {
+ function Project() {
+ $('ul.clone-options-dropdown a').click(function() {
+ var url;
+ if ($(this).hasClass('active')) {
+ return;
+ }
+ $('.active').not($(this)).removeClass('active');
+ $(this).toggleClass('active');
+ url = $("#project_clone").val();
+ $('#project_clone').val(url);
+ return $('.clone').text(url);
+ });
+ this.initRefSwitcher();
+ $('.project-refs-select').on('change', function() {
+ return $(this).parents('form').submit();
+ });
+ $('.hide-no-ssh-message').on('click', function(e) {
+ $.cookie('hide_no_ssh_message', 'false', {
+ path: gon.relative_url_root || '/'
+ });
+ $(this).parents('.no-ssh-key-message').remove();
+ return e.preventDefault();
+ });
+ $('.hide-no-password-message').on('click', function(e) {
+ $.cookie('hide_no_password_message', 'false', {
+ path: gon.relative_url_root || '/'
+ });
+ $(this).parents('.no-password-message').remove();
+ return e.preventDefault();
+ });
+ this.projectSelectDropdown();
+ }
+
+ Project.prototype.projectSelectDropdown = function() {
+ new ProjectSelect();
+ $('.project-item-select').on('click', (function(_this) {
+ return function(e) {
+ return _this.changeProject($(e.currentTarget).val());
+ };
+ })(this));
+ return $('.js-projects-dropdown-toggle').on('click', function(e) {
+ e.preventDefault();
+ return $('.js-projects-dropdown').select2('open');
+ });
+ };
+
+ Project.prototype.changeProject = function(url) {
+ return window.location = url;
+ };
+
+ Project.prototype.initRefSwitcher = function() {
+ return $('.js-project-refs-dropdown').each(function() {
+ var $dropdown, selected;
+ $dropdown = $(this);
+ selected = $dropdown.data('selected');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ return $.ajax({
+ url: $dropdown.data('refs-url'),
+ data: {
+ ref: $dropdown.data('ref')
+ },
+ dataType: "json"
+ }).done(function(refs) {
+ return callback(refs);
+ });
+ },
+ selectable: true,
+ filterable: true,
+ filterByText: true,
+ fieldName: $dropdown.data('field-name'),
+ renderRow: function(ref) {
+ var link;
+ if (ref.header != null) {
+ return $('<li />').addClass('dropdown-header').text(ref.header);
+ } else {
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ return $('<li />').append(link);
+ }
+ },
+ id: function(obj, $el) {
+ return $el.attr('data-ref');
+ },
+ toggleLabel: function(obj, $el) {
+ return $el.text().trim();
+ },
+ clicked: function(selected, $el, e) {
+ e.preventDefault()
+ if ($('input[name="ref"]').length) {
+ var $form = $dropdown.closest('form'),
+ action = $form.attr('action'),
+ divider = action.indexOf('?') < 0 ? '?' : '&';
+ Turbolinks.visit(action + '' + divider + '' + $form.serialize());
+ }
+ }
+ });
+ });
+ };
+
+ return Project;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
deleted file mode 100644
index 3288c801388..00000000000
--- a/app/assets/javascripts/project.js.coffee
+++ /dev/null
@@ -1,91 +0,0 @@
-class @Project
- constructor: ->
- # Git protocol switcher
- $('ul.clone-options-dropdown a').click ->
- return if $(@).hasClass('active')
-
-
- # Remove the active class for all buttons (ssh, http, kerberos if shown)
- $('.active').not($(@)).removeClass('active');
- # Add the active class for the clicked button
- $(@).toggleClass('active')
-
- url = $("#project_clone").val()
-
- # Update the input field
- $('#project_clone').val(url)
-
- # Update the command line instructions
- $('.clone').text(url)
-
- # Ref switcher
- @initRefSwitcher()
- $('.project-refs-select').on 'change', ->
- $(@).parents('form').submit()
-
- $('.hide-no-ssh-message').on 'click', (e) ->
- path = '/'
- $.cookie('hide_no_ssh_message', 'false', { path: path })
- $(@).parents('.no-ssh-key-message').remove()
- e.preventDefault()
-
- $('.hide-no-password-message').on 'click', (e) ->
- path = '/'
- $.cookie('hide_no_password_message', 'false', { path: path })
- $(@).parents('.no-password-message').remove()
- e.preventDefault()
-
- @projectSelectDropdown()
-
- projectSelectDropdown: ->
- new ProjectSelect()
-
- $('.project-item-select').on 'click', (e) =>
- @changeProject $(e.currentTarget).val()
-
- $('.js-projects-dropdown-toggle').on 'click', (e) ->
- e.preventDefault()
-
- $('.js-projects-dropdown').select2('open')
-
- changeProject: (url) ->
- window.location = url
-
- initRefSwitcher: ->
- $('.js-project-refs-dropdown').each ->
- $dropdown = $(@)
- selected = $dropdown.data('selected')
-
- $dropdown.glDropdown(
- data: (term, callback) ->
- $.ajax(
- url: $dropdown.data('refs-url')
- data:
- ref: $dropdown.data('ref')
- ).done (refs) ->
- callback(refs)
- selectable: true
- filterable: true
- filterByText: true
- fieldName: 'ref'
- renderRow: (ref) ->
- if ref.header?
- $('<li />')
- .addClass('dropdown-header')
- .text(ref.header)
- else
- link = $('<a />')
- .attr('href', '#')
- .addClass(if ref is selected then 'is-active' else '')
- .text(ref)
- .attr('data-ref', escape(ref))
-
- $('<li />')
- .append(link)
- id: (obj, $el) ->
- $el.attr('data-ref')
- toggleLabel: (obj, $el) ->
- $el.text().trim()
- clicked: (e) ->
- $dropdown.closest('form').submit()
- )
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
new file mode 100644
index 00000000000..277e71523d5
--- /dev/null
+++ b/app/assets/javascripts/project_avatar.js
@@ -0,0 +1,21 @@
+(function() {
+ this.ProjectAvatar = (function() {
+ function ProjectAvatar() {
+ $('.js-choose-project-avatar-button').bind('click', function() {
+ var form;
+ form = $(this).closest('form');
+ return form.find('.js-project-avatar-input').click();
+ });
+ $('.js-project-avatar-input').bind('change', function() {
+ var filename, form;
+ form = $(this).closest('form');
+ filename = $(this).val().replace(/^.*[\\\/]/, '');
+ return form.find('.js-avatar-filename').text(filename);
+ });
+ }
+
+ return ProjectAvatar;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_avatar.js.coffee b/app/assets/javascripts/project_avatar.js.coffee
deleted file mode 100644
index 8bec6e2ccca..00000000000
--- a/app/assets/javascripts/project_avatar.js.coffee
+++ /dev/null
@@ -1,9 +0,0 @@
-class @ProjectAvatar
- constructor: ->
- $('.js-choose-project-avatar-button').bind 'click', ->
- form = $(this).closest('form')
- form.find('.js-project-avatar-input').click()
- $('.js-project-avatar-input').bind 'change', ->
- form = $(this).closest('form')
- filename = $(this).val().replace(/^.*[\\\/]/, '')
- form.find('.js-avatar-filename').text(filename)
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
new file mode 100644
index 00000000000..4925f0519f0
--- /dev/null
+++ b/app/assets/javascripts/project_find_file.js
@@ -0,0 +1,170 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.ProjectFindFile = (function() {
+ var highlighter;
+
+ function ProjectFindFile(element1, options) {
+ this.element = element1;
+ this.options = options;
+ this.goToBlob = bind(this.goToBlob, this);
+ this.goToTree = bind(this.goToTree, this);
+ this.selectRowDown = bind(this.selectRowDown, this);
+ this.selectRowUp = bind(this.selectRowUp, this);
+ this.filePaths = {};
+ this.inputElement = this.element.find(".file-finder-input");
+ this.initEvent();
+ this.inputElement.focus();
+ this.load(this.options.url);
+ }
+
+ ProjectFindFile.prototype.initEvent = function() {
+ this.inputElement.off("keyup");
+ this.inputElement.on("keyup", (function(_this) {
+ return function(event) {
+ var oldValue, ref, target, value;
+ target = $(event.target);
+ value = target.val();
+ oldValue = (ref = target.data("oldValue")) != null ? ref : "";
+ if (value !== oldValue) {
+ target.data("oldValue", value);
+ _this.findFile();
+ return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus();
+ }
+ };
+ })(this));
+ return this.element.find(".tree-content-holder .tree-table").on("click", function(event) {
+ var path;
+ if (event.target.nodeName !== "A") {
+ path = this.element.find(".tree-item-file-name a", this).attr("href");
+ if (path) {
+ return location.href = path;
+ }
+ }
+ });
+ };
+
+ ProjectFindFile.prototype.findFile = function() {
+ var result, searchText;
+ searchText = this.inputElement.val();
+ result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
+ return this.renderList(result, searchText);
+ };
+
+ ProjectFindFile.prototype.load = function(url) {
+ return $.ajax({
+ url: url,
+ method: "get",
+ dataType: "json",
+ success: (function(_this) {
+ return function(data) {
+ _this.element.find(".loading").hide();
+ _this.filePaths = data;
+ _this.findFile();
+ return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus();
+ };
+ })(this)
+ });
+ };
+
+ ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
+ var blobItemUrl, filePath, html, i, j, len, matches, results;
+ this.element.find(".tree-table > tbody").empty();
+ results = [];
+ for (i = j = 0, len = filePaths.length; j < len; i = ++j) {
+ filePath = filePaths[i];
+ if (i === 20) {
+ break;
+ }
+ if (searchText) {
+ matches = fuzzaldrinPlus.match(filePath, searchText);
+ }
+ blobItemUrl = this.options.blobUrlTemplate + "/" + filePath;
+ html = this.makeHtml(filePath, matches, blobItemUrl);
+ results.push(this.element.find(".tree-table > tbody").append(html));
+ }
+ return results;
+ };
+
+ highlighter = function(element, text, matches) {
+ var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
+ lastIndex = 0;
+ highlightText = "";
+ matchedChars = [];
+ for (j = 0, len = matches.length; j < len; j++) {
+ matchIndex = matches[j];
+ unmatched = text.substring(lastIndex, matchIndex);
+ if (unmatched) {
+ if (matchedChars.length) {
+ element.append(matchedChars.join("").bold());
+ }
+ matchedChars = [];
+ element.append(document.createTextNode(unmatched));
+ }
+ matchedChars.push(text[matchIndex]);
+ lastIndex = matchIndex + 1;
+ }
+ if (matchedChars.length) {
+ element.append(matchedChars.join("").bold());
+ }
+ return element.append(document.createTextNode(text.substring(lastIndex)));
+ };
+
+ ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
+ var $tr;
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
+ if (matches) {
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl));
+ } else {
+ $tr.find("a").attr("href", blobItemUrl).text(filePath);
+ }
+ return $tr;
+ };
+
+ ProjectFindFile.prototype.selectRow = function(type) {
+ var next, rows, selectedRow;
+ rows = this.element.find(".files-slider tr.tree-item");
+ selectedRow = this.element.find(".files-slider tr.tree-item.selected");
+ if (rows && rows.length > 0) {
+ if (selectedRow && selectedRow.length > 0) {
+ if (type === "UP") {
+ next = selectedRow.prev();
+ } else if (type === "DOWN") {
+ next = selectedRow.next();
+ }
+ if (next.length > 0) {
+ selectedRow.removeClass("selected");
+ selectedRow = next;
+ }
+ } else {
+ selectedRow = rows.eq(0);
+ }
+ return selectedRow.addClass("selected").focus();
+ }
+ };
+
+ ProjectFindFile.prototype.selectRowUp = function() {
+ return this.selectRow("UP");
+ };
+
+ ProjectFindFile.prototype.selectRowDown = function() {
+ return this.selectRow("DOWN");
+ };
+
+ ProjectFindFile.prototype.goToTree = function() {
+ return location.href = this.options.treeUrl;
+ };
+
+ ProjectFindFile.prototype.goToBlob = function() {
+ var path;
+ path = this.element.find(".tree-item.selected .tree-item-file-name a").attr("href");
+ if (path) {
+ return location.href = path;
+ }
+ };
+
+ return ProjectFindFile;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee
deleted file mode 100644
index 0dd32352c34..00000000000
--- a/app/assets/javascripts/project_find_file.js.coffee
+++ /dev/null
@@ -1,125 +0,0 @@
-class @ProjectFindFile
- constructor: (@element, @options)->
- @filePaths = {}
- @inputElement = @element.find(".file-finder-input")
-
- # init event
- @initEvent()
-
- # focus text input box
- @inputElement.focus()
-
- # load file list
- @load(@options.url)
-
- # init event
- initEvent: ->
- @inputElement.off "keyup"
- @inputElement.on "keyup", (event) =>
- target = $(event.target)
- value = target.val()
- oldValue = target.data("oldValue") ? ""
-
- if value != oldValue
- target.data("oldValue", value)
- @findFile()
- @element.find("tr.tree-item").eq(0).addClass("selected").focus()
-
- @element.find(".tree-content-holder .tree-table").on "click", (event) ->
- if (event.target.nodeName != "A")
- path = @element.find(".tree-item-file-name a", this).attr("href")
- location.href = path if path
-
- # find file
- findFile: ->
- searchText = @inputElement.val()
- result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
- @renderList result, searchText
-
- # files pathes load
- load: (url) ->
- $.ajax
- url: url
- method: "get"
- dataType: "json"
- success: (data) =>
- @element.find(".loading").hide()
- @filePaths = data
- @findFile()
- @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
-
- # render result
- renderList: (filePaths, searchText) ->
- @element.find(".tree-table > tbody").empty()
-
- for filePath, i in filePaths
- break if i == 20
-
- if searchText
- matches = fuzzaldrinPlus.match(filePath, searchText)
-
- blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
-
- html = @makeHtml filePath, matches, blobItemUrl
- @element.find(".tree-table > tbody").append(html)
-
- # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
- highlighter = (element, text, matches) ->
- lastIndex = 0
- highlightText = ""
- matchedChars = []
-
- for matchIndex in matches
- unmatched = text.substring(lastIndex, matchIndex)
-
- if unmatched
- element.append(matchedChars.join("").bold()) if matchedChars.length
- matchedChars = []
- element.append(document.createTextNode(unmatched))
-
- matchedChars.push(text[matchIndex])
- lastIndex = matchIndex + 1
-
- element.append(matchedChars.join("").bold()) if matchedChars.length
- element.append(document.createTextNode(text.substring(lastIndex)))
-
- # make tbody row html
- makeHtml: (filePath, matches, blobItemUrl) ->
- $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
- if matches
- $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
- else
- $tr.find("a").attr("href", blobItemUrl).text(filePath)
-
- return $tr
-
- selectRow: (type) ->
- rows = @element.find(".files-slider tr.tree-item")
- selectedRow = @element.find(".files-slider tr.tree-item.selected")
-
- if rows && rows.length > 0
- if selectedRow && selectedRow.length > 0
- if type == "UP"
- next = selectedRow.prev()
- else if type == "DOWN"
- next = selectedRow.next()
-
- if next.length > 0
- selectedRow.removeClass "selected"
- selectedRow = next
- else
- selectedRow = rows.eq(0)
- selectedRow.addClass("selected").focus()
-
- selectRowUp: =>
- @selectRow "UP"
-
- selectRowDown: =>
- @selectRow "DOWN"
-
- goToTree: =>
- location.href = @options.treeUrl
-
- goToBlob: =>
- path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
- location.href = path if path
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
new file mode 100644
index 00000000000..d2261c51f35
--- /dev/null
+++ b/app/assets/javascripts/project_fork.js
@@ -0,0 +1,14 @@
+(function() {
+ this.ProjectFork = (function() {
+ function ProjectFork() {
+ $('.fork-thumbnail a').on('click', function() {
+ $('.fork-namespaces').hide();
+ return $('.save-project-loader').show();
+ });
+ }
+
+ return ProjectFork;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_fork.js.coffee b/app/assets/javascripts/project_fork.js.coffee
deleted file mode 100644
index e15a1c4ef76..00000000000
--- a/app/assets/javascripts/project_fork.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-class @ProjectFork
- constructor: ->
- $('.fork-thumbnail a').on 'click', ->
- $('.fork-namespaces').hide()
- $('.save-project-loader').show()
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
new file mode 100644
index 00000000000..c61b0cf2fde
--- /dev/null
+++ b/app/assets/javascripts/project_import.js
@@ -0,0 +1,13 @@
+(function() {
+ this.ProjectImport = (function() {
+ function ProjectImport() {
+ setTimeout(function() {
+ return Turbolinks.visit(location.href);
+ }, 5000);
+ }
+
+ return ProjectImport;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_import.js.coffee b/app/assets/javascripts/project_import.js.coffee
deleted file mode 100644
index 6633564a079..00000000000
--- a/app/assets/javascripts/project_import.js.coffee
+++ /dev/null
@@ -1,5 +0,0 @@
-class @ProjectImport
- constructor: ->
- setTimeout ->
- Turbolinks.visit(location.href)
- , 5000
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
new file mode 100644
index 00000000000..78f7b48bc7d
--- /dev/null
+++ b/app/assets/javascripts/project_members.js
@@ -0,0 +1,10 @@
+(function() {
+ this.ProjectMembers = (function() {
+ function ProjectMembers() {
+ $('li.project_member').bind('ajax:success', function() {
+ return $(this).fadeOut();
+ });
+ }
+ return ProjectMembers;
+ })();
+}).call(this);
diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee
deleted file mode 100644
index 896ba7e53ee..00000000000
--- a/app/assets/javascripts/project_members.js.coffee
+++ /dev/null
@@ -1,4 +0,0 @@
-class @ProjectMembers
- constructor: ->
- $('li.project_member').bind 'ajax:success', ->
- $(this).fadeOut()
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
new file mode 100644
index 00000000000..798f15e40a0
--- /dev/null
+++ b/app/assets/javascripts/project_new.js
@@ -0,0 +1,40 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.ProjectNew = (function() {
+ function ProjectNew() {
+ this.toggleSettings = bind(this.toggleSettings, this);
+ $('.project-edit-container').on('ajax:before', (function(_this) {
+ return function() {
+ $('.project-edit-container').hide();
+ return $('.save-project-loader').show();
+ };
+ })(this));
+ this.toggleSettings();
+ this.toggleSettingsOnclick();
+ }
+
+ ProjectNew.prototype.toggleSettings = function() {
+ this._showOrHide('#project_builds_enabled', '.builds-feature');
+ return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature');
+ };
+
+ ProjectNew.prototype.toggleSettingsOnclick = function() {
+ return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings);
+ };
+
+ ProjectNew.prototype._showOrHide = function(checkElement, container) {
+ var $container;
+ $container = $(container);
+ if ($(checkElement).prop('checked')) {
+ return $container.show();
+ } else {
+ return $container.hide();
+ }
+ };
+
+ return ProjectNew;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_new.js.coffee b/app/assets/javascripts/project_new.js.coffee
deleted file mode 100644
index e48343a19b5..00000000000
--- a/app/assets/javascripts/project_new.js.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-class @ProjectNew
- constructor: ->
- $('.project-edit-container').on 'ajax:before', =>
- $('.project-edit-container').hide()
- $('.save-project-loader').show()
- @toggleSettings()
- @toggleSettingsOnclick()
-
-
- toggleSettings: =>
- @_showOrHide('#project_builds_enabled', '.builds-feature')
- @_showOrHide('#project_merge_requests_enabled', '.merge-requests-feature')
-
- toggleSettingsOnclick: ->
- $('#project_builds_enabled, #project_merge_requests_enabled').on 'click', @toggleSettings
-
- _showOrHide: (checkElement, container) ->
- $container = $(container)
-
- if $(checkElement).prop('checked')
- $container.show()
- else
- $container.hide()
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
new file mode 100644
index 00000000000..20b147500cf
--- /dev/null
+++ b/app/assets/javascripts/project_select.js
@@ -0,0 +1,102 @@
+(function() {
+ this.ProjectSelect = (function() {
+ function ProjectSelect() {
+ $('.js-projects-dropdown-toggle').each(function(i, dropdown) {
+ var $dropdown;
+ $dropdown = $(dropdown);
+ return $dropdown.glDropdown({
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name_with_namespace']
+ },
+ data: function(term, callback) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
+ return callback(projects);
+ };
+ if (this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(term, false, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (this.groupId) {
+ return Api.groupProjects(this.groupId, term, projectsCallback);
+ } else {
+ return Api.projects(term, this.orderBy, projectsCallback);
+ }
+ },
+ url: function(project) {
+ return project.web_url;
+ },
+ text: function(project) {
+ return project.name_with_namespace;
+ }
+ });
+ });
+ $('.ajax-project-select').each(function(i, select) {
+ var placeholder;
+ this.groupId = $(select).data('group-id');
+ this.includeGroups = $(select).data('include-groups');
+ this.orderBy = $(select).data('order-by') || 'id';
+ placeholder = "Search for project";
+ if (this.includeGroups) {
+ placeholder += " or group";
+ }
+ return $(select).select2({
+ placeholder: placeholder,
+ minimumInputLength: 0,
+ query: (function(_this) {
+ return function(query) {
+ var finalCallback, projectsCallback;
+ finalCallback = function(projects) {
+ var data;
+ data = {
+ results: projects
+ };
+ return query.callback(data);
+ };
+ if (_this.includeGroups) {
+ projectsCallback = function(projects) {
+ var groupsCallback;
+ groupsCallback = function(groups) {
+ var data;
+ data = groups.concat(projects);
+ return finalCallback(data);
+ };
+ return Api.groups(query.term, false, groupsCallback);
+ };
+ } else {
+ projectsCallback = finalCallback;
+ }
+ if (_this.groupId) {
+ return Api.groupProjects(_this.groupId, query.term, projectsCallback);
+ } else {
+ return Api.projects(query.term, _this.orderBy, projectsCallback);
+ }
+ };
+ })(this),
+ id: function(project) {
+ return project.web_url;
+ },
+ text: function(project) {
+ return project.name_with_namespace || project.name;
+ },
+ dropdownCssClass: "ajax-project-dropdown"
+ });
+ });
+ }
+
+ return ProjectSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee
deleted file mode 100644
index 704bd8dee53..00000000000
--- a/app/assets/javascripts/project_select.js.coffee
+++ /dev/null
@@ -1,72 +0,0 @@
-class @ProjectSelect
- constructor: ->
- $('.js-projects-dropdown-toggle').each (i, dropdown) ->
- $dropdown = $(dropdown)
-
- $dropdown.glDropdown(
- filterable: true
- filterRemote: true
- search:
- fields: ['name_with_namespace']
- data: (term, callback) ->
- finalCallback = (projects) ->
- callback projects
-
- if @includeGroups
- projectsCallback = (projects) ->
- groupsCallback = (groups) ->
- data = groups.concat(projects)
- finalCallback(data)
-
- Api.groups term, false, groupsCallback
- else
- projectsCallback = finalCallback
-
- if @groupId
- Api.groupProjects @groupId, term, projectsCallback
- else
- Api.projects term, @orderBy, projectsCallback
- url: (project) ->
- project.web_url
- text: (project) ->
- project.name_with_namespace
- )
-
- $('.ajax-project-select').each (i, select) ->
- @groupId = $(select).data('group-id')
- @includeGroups = $(select).data('include-groups')
- @orderBy = $(select).data('order-by') || 'id'
-
- placeholder = "Search for project"
- placeholder += " or group" if @includeGroups
-
- $(select).select2
- placeholder: placeholder
- minimumInputLength: 0
- query: (query) =>
- finalCallback = (projects) ->
- data = { results: projects }
- query.callback(data)
-
- if @includeGroups
- projectsCallback = (projects) ->
- groupsCallback = (groups) ->
- data = groups.concat(projects)
- finalCallback(data)
-
- Api.groups query.term, false, groupsCallback
- else
- projectsCallback = finalCallback
-
- if @groupId
- Api.groupProjects @groupId, query.term, projectsCallback
- else
- Api.projects query.term, @orderBy, projectsCallback
-
- id: (project) ->
- project.web_url
-
- text: (project) ->
- project.name_with_namespace || project.name
-
- dropdownCssClass: "ajax-project-dropdown"
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
new file mode 100644
index 00000000000..8ca4c427912
--- /dev/null
+++ b/app/assets/javascripts/project_show.js
@@ -0,0 +1,9 @@
+(function() {
+ this.ProjectShow = (function() {
+ function ProjectShow() {}
+
+ return ProjectShow;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/project_show.js.coffee b/app/assets/javascripts/project_show.js.coffee
deleted file mode 100644
index 1fdf28f2528..00000000000
--- a/app/assets/javascripts/project_show.js.coffee
+++ /dev/null
@@ -1,3 +0,0 @@
-class @ProjectShow
- constructor: ->
- # I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
new file mode 100644
index 00000000000..4f415b05dbc
--- /dev/null
+++ b/app/assets/javascripts/projects_list.js
@@ -0,0 +1,48 @@
+(function() {
+ this.ProjectsList = {
+ init: function() {
+ $(".projects-list-filter").off('keyup');
+ this.initSearch();
+ return this.initPagination();
+ },
+ initSearch: function() {
+ var debounceFilter, projectsListFilter;
+ projectsListFilter = $('.projects-list-filter');
+ debounceFilter = _.debounce(ProjectsList.filterResults, 500);
+ return projectsListFilter.on('keyup', function(e) {
+ if (projectsListFilter.val() !== '') {
+ return debounceFilter();
+ }
+ });
+ },
+ filterResults: function() {
+ var form, project_filter_url, search;
+ $('.projects-list-holder').fadeTo(250, 0.5);
+ form = null;
+ form = $("form#project-filter-form");
+ search = $(".projects-list-filter").val();
+ project_filter_url = form.attr('action') + '?' + form.serialize();
+ return $.ajax({
+ type: "GET",
+ url: form.attr('action'),
+ data: form.serialize(),
+ complete: function() {
+ return $('.projects-list-holder').fadeTo(250, 1);
+ },
+ success: function(data) {
+ $('.projects-list-holder').replaceWith(data.html);
+ return history.replaceState({
+ page: project_filter_url
+ }, document.title, project_filter_url);
+ },
+ dataType: "json"
+ });
+ },
+ initPagination: function() {
+ return $('.projects-list-holder .pagination').on('ajax:success', function(e, data) {
+ return $('.projects-list-holder').replaceWith(data.html);
+ });
+ }
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
deleted file mode 100644
index e4c4bf3b273..00000000000
--- a/app/assets/javascripts/projects_list.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-@ProjectsList =
- init: ->
- $(".projects-list-filter").off('keyup')
- this.initSearch()
- this.initPagination()
-
- initSearch: ->
- @timer = null
- $(".projects-list-filter").on('keyup', ->
- clearTimeout(@timer)
- @timer = setTimeout(ProjectsList.filterResults, 500)
- )
-
- filterResults: =>
- $('.projects-list-holder').fadeTo(250, 0.5)
-
- form = null
- form = $("form#project-filter-form")
- search = $(".projects-list-filter").val()
- project_filter_url = form.attr('action') + '?' + form.serialize()
-
- $.ajax
- type: "GET"
- url: form.attr('action')
- data: form.serialize()
- complete: ->
- $('.projects-list-holder').fadeTo(250, 1)
- success: (data) ->
- $('.projects-list-holder').replaceWith(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: project_filter_url}, document.title, project_filter_url
- dataType: "json"
-
- initPagination: ->
- $('.projects-list-holder .pagination').on('ajax:success', (e, data) ->
- $('.projects-list-holder').replaceWith(data.html)
- )
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
new file mode 100644
index 00000000000..7aeb5f92514
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
@@ -0,0 +1,28 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.ProtectedBranchAccessDropdown = class {
+ constructor(options) {
+ const { $dropdown, data, onSelect } = options;
+
+ $dropdown.glDropdown({
+ data: data,
+ selectable: true,
+ inputId: $dropdown.data('input-id'),
+ fieldName: $dropdown.data('field-name'),
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ } else {
+ return 'Select';
+ }
+ },
+ clicked(item, $el, e) {
+ e.preventDefault();
+ onSelect();
+ }
+ });
+ }
+ }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
new file mode 100644
index 00000000000..46beca469b9
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -0,0 +1,54 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.ProtectedBranchCreate = class {
+ constructor() {
+ this.$wrap = this.$form = $('#new_protected_branch');
+ this.buildDropdowns();
+ }
+
+ buildDropdowns() {
+ const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+ const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+
+ // Cache callback
+ this.onSelectCallback = this.onSelect.bind(this);
+
+ // Allowed to Merge dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: $allowedToMergeDropdown,
+ data: gon.merge_access_levels,
+ onSelect: this.onSelectCallback
+ });
+
+ // Allowed to Push dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: $allowedToPushDropdown,
+ data: gon.push_access_levels,
+ onSelect: this.onSelectCallback
+ });
+
+ // Select default
+ $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
+ $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
+
+ // Protected branch dropdown
+ new ProtectedBranchDropdown({
+ $dropdown: this.$wrap.find('.js-protected-branch-select'),
+ onSelect: this.onSelectCallback
+ });
+ }
+
+ // This will run after clicked callback
+ onSelect() {
+
+ // Enable submit button
+ const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
+ const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
+ const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
+
+ this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
+ }
+ }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6
new file mode 100644
index 00000000000..6738dc8862d
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_dropdown.js.es6
@@ -0,0 +1,75 @@
+class ProtectedBranchDropdown {
+ constructor(options) {
+ this.onSelect = options.onSelect;
+ this.$dropdown = options.$dropdown;
+ this.$dropdownContainer = this.$dropdown.parent();
+ this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
+ this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+
+ this.buildDropdown();
+ this.bindEvents();
+
+ // Hide footer
+ this.$dropdownFooter.addClass('hidden');
+ }
+
+ buildDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.getProtectedBranches.bind(this),
+ filterable: true,
+ remote: false,
+ search: {
+ fields: ['title']
+ },
+ selectable: true,
+ toggleLabel(selected) {
+ return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
+ },
+ fieldName: 'protected_branch[name]',
+ text(protectedBranch) {
+ return _.escape(protectedBranch.title);
+ },
+ id(protectedBranch) {
+ return _.escape(protectedBranch.id);
+ },
+ onFilter: this.toggleCreateNewButton.bind(this),
+ clicked: (item, $el, e) => {
+ e.preventDefault();
+ this.onSelect();
+ }
+ });
+ }
+
+ bindEvents() {
+ this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
+ }
+
+ onClickCreateWildcard() {
+ this.$dropdown.data('glDropdown').remote.execute();
+ this.$dropdown.data('glDropdown').selectRowAtIndex(0);
+ }
+
+ getProtectedBranches(term, callback) {
+ if (this.selectedBranch) {
+ callback(gon.open_branches.concat(this.selectedBranch));
+ } else {
+ callback(gon.open_branches);
+ }
+ }
+
+ toggleCreateNewButton(branchName) {
+ this.selectedBranch = {
+ title: branchName,
+ id: branchName,
+ text: branchName
+ };
+
+ if (branchName) {
+ this.$dropdownContainer
+ .find('.create-new-protected-branch code')
+ .text(branchName);
+ }
+
+ this.$dropdownFooter.toggleClass('hidden', !branchName);
+ }
+}
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
new file mode 100644
index 00000000000..40bc4adb71b
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -0,0 +1,66 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.ProtectedBranchEdit = class {
+ constructor(options) {
+ this.$wrap = options.$wrap;
+ this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
+ this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
+
+ this.buildDropdowns();
+ }
+
+ buildDropdowns() {
+
+ // Allowed to merge dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToMergeDropdown,
+ data: gon.merge_access_levels,
+ onSelect: this.onSelect.bind(this)
+ });
+
+ // Allowed to push dropdown
+ new gl.ProtectedBranchAccessDropdown({
+ $dropdown: this.$allowedToPushDropdown,
+ data: gon.push_access_levels,
+ onSelect: this.onSelect.bind(this)
+ });
+ }
+
+ onSelect() {
+ const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
+ 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;
+
+ $.ajax({
+ type: 'POST',
+ url: this.$wrap.data('url'),
+ dataType: 'json',
+ data: {
+ _method: 'PATCH',
+ id: this.$wrap.data('banchId'),
+ protected_branch: {
+ merge_access_levels_attributes: [{
+ id: this.$allowedToMergeDropdown.data('access-level-id'),
+ access_level: $allowedToMergeInput.val()
+ }],
+ push_access_levels_attributes: [{
+ id: this.$allowedToPushDropdown.data('access-level-id'),
+ access_level: $allowedToPushInput.val()
+ }]
+ }
+ },
+ success: () => {
+ this.$wrap.effect('highlight');
+ },
+ error() {
+ $.scrollTo(0);
+ new Flash('Failed to update branch!');
+ }
+ });
+ }
+ }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6
new file mode 100644
index 00000000000..9ff0fd12c76
--- /dev/null
+++ b/app/assets/javascripts/protected_branch_edit_list.js.es6
@@ -0,0 +1,17 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.ProtectedBranchEditList = class {
+ constructor() {
+ this.$wrap = $('.protected-branches-list');
+
+ // Build edit forms
+ this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
+ new gl.ProtectedBranchEdit({
+ $wrap: $(el)
+ });
+ });
+ }
+ }
+
+})(window);
diff --git a/app/assets/javascripts/protected_branch_select.js.coffee b/app/assets/javascripts/protected_branch_select.js.coffee
deleted file mode 100644
index 6d45770ace9..00000000000
--- a/app/assets/javascripts/protected_branch_select.js.coffee
+++ /dev/null
@@ -1,40 +0,0 @@
-class @ProtectedBranchSelect
- constructor: (currentProject) ->
- $('.dropdown-footer').hide();
- @dropdown = $('.js-protected-branch-select').glDropdown(
- data: @getProtectedBranches
- filterable: true
- remote: false
- search:
- fields: ['title']
- selectable: true
- toggleLabel: (selected) -> if (selected and 'id' of selected) then selected.title else 'Protected Branch'
- fieldName: 'protected_branch[name]'
- text: (protected_branch) -> _.escape(protected_branch.title)
- id: (protected_branch) -> _.escape(protected_branch.id)
- onFilter: @toggleCreateNewButton
- clicked: () -> $('.protect-branch-btn').attr('disabled', false)
- )
-
- $('.create-new-protected-branch').on 'click', (event) =>
- # Refresh the dropdown's data, which ends up calling `getProtectedBranches`
- @dropdown.data('glDropdown').remote.execute()
- @dropdown.data('glDropdown').selectRowAtIndex(event, 0)
-
- getProtectedBranches: (term, callback) =>
- if @selectedBranch
- callback(gon.open_branches.concat(@selectedBranch))
- else
- callback(gon.open_branches)
-
- toggleCreateNewButton: (branchName) =>
- @selectedBranch = { title: branchName, id: branchName, text: branchName }
-
- if branchName is ''
- $('.protected-branch-select-footer-list').addClass('hidden')
- $('.dropdown-footer').hide();
- else
- $('.create-new-protected-branch').text("Create Protected Branch: #{branchName}")
- $('.protected-branch-select-footer-list').removeClass('hidden')
- $('.dropdown-footer').show();
-
diff --git a/app/assets/javascripts/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee
deleted file mode 100644
index 79c2306e4d2..00000000000
--- a/app/assets/javascripts/protected_branches.js.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-$ ->
- $(".protected-branches-list :checkbox").change (e) ->
- name = $(this).attr("name")
- if name == "developers_can_push"
- id = $(this).val()
- checked = $(this).is(":checked")
- url = $(this).data("url")
- $.ajax
- type: "PUT"
- url: url
- dataType: "json"
- data:
- id: id
- protected_branch:
- developers_can_push: checked
-
- success: ->
- row = $(e.target)
- row.closest('tr').effect('highlight')
-
- error: ->
- new Flash("Failed to update branch!", "alert")
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
new file mode 100644
index 00000000000..e3d5f413c77
--- /dev/null
+++ b/app/assets/javascripts/right_sidebar.js
@@ -0,0 +1,201 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Sidebar = (function() {
+ function Sidebar(currentUser) {
+ this.toggleTodo = bind(this.toggleTodo, this);
+ this.sidebar = $('aside');
+ this.addEventListeners();
+ }
+
+ Sidebar.prototype.addEventListeners = function() {
+ this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
+ $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
+ $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
+ $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
+ $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
+ var $allGutterToggleIcons, $this, $thisIcon;
+ e.preventDefault();
+ $this = $(this);
+ $thisIcon = $this.find('i');
+ $allGutterToggleIcons = $('.js-sidebar-toggle i');
+ if ($thisIcon.hasClass('fa-angle-double-right')) {
+ $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left');
+ $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed');
+ } else {
+ $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right');
+ $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
+ }
+ if (!triggered) {
+ return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
+ path: gon.relative_url_root || '/'
+ });
+ }
+ });
+ return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
+ };
+
+ Sidebar.prototype.toggleTodo = function(e) {
+ var $btnText, $this, $todoLoading, ajaxType, url;
+ $this = $(e.currentTarget);
+ $todoLoading = $('.js-issuable-todo-loading');
+ $btnText = $('.js-issuable-todo-text', $this);
+ ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
+ if ($this.attr('data-delete-path')) {
+ url = "" + ($this.attr('data-delete-path'));
+ } else {
+ url = "" + ($this.data('url'));
+ }
+ return $.ajax({
+ url: url,
+ type: ajaxType,
+ dataType: 'json',
+ data: {
+ issuable_id: $this.data('issuable-id'),
+ issuable_type: $this.data('issuable-type')
+ },
+ beforeSend: (function(_this) {
+ return function() {
+ return _this.beforeTodoSend($this, $todoLoading);
+ };
+ })(this)
+ }).done((function(_this) {
+ return function(data) {
+ return _this.todoUpdateDone(data, $this, $btnText, $todoLoading);
+ };
+ })(this));
+ };
+
+ Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) {
+ $btn.disable();
+ return $todoLoading.removeClass('hidden');
+ };
+
+ Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
+ var $todoPendingCount;
+ $todoPendingCount = $('.todos-pending-count');
+ $todoPendingCount.text(data.count);
+ $btn.enable();
+ $todoLoading.addClass('hidden');
+ if (data.count === 0) {
+ $todoPendingCount.addClass('hidden');
+ } else {
+ $todoPendingCount.removeClass('hidden');
+ }
+ if (data.delete_path != null) {
+ $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
+ return $btnText.text($btn.data('mark-text'));
+ } else {
+ $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path');
+ return $btnText.text($btn.data('todo-text'));
+ }
+ };
+
+ Sidebar.prototype.sidebarDropdownLoading = function(e) {
+ var $loading, $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ i = $sidebarCollapsedIcon.find('i');
+ $loading = $('<i class="fa fa-spinner fa-spin"></i>');
+ if (img.length) {
+ img.before($loading);
+ return img.hide();
+ } else if (i.length) {
+ i.before($loading);
+ return i.hide();
+ }
+ };
+
+ Sidebar.prototype.sidebarDropdownLoaded = function(e) {
+ var $sidebarCollapsedIcon, i, img;
+ $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon');
+ img = $sidebarCollapsedIcon.find('img');
+ $sidebarCollapsedIcon.find('i.fa-spin').remove();
+ i = $sidebarCollapsedIcon.find('i');
+ if (img.length) {
+ return img.show();
+ } else {
+ return i.show();
+ }
+ };
+
+ Sidebar.prototype.sidebarCollapseClicked = function(e) {
+ var $block, sidebar;
+ if ($(e.currentTarget).hasClass('dont-change-state')) {
+ return;
+ }
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(this).closest('.block');
+ return sidebar.openDropdown($block);
+ };
+
+ Sidebar.prototype.openDropdown = function(blockOrName) {
+ var $block;
+ $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName;
+ $block.find('.edit-link').trigger('click');
+ if (!this.isOpen()) {
+ this.setCollapseAfterUpdate($block);
+ return this.toggleSidebar('open');
+ }
+ };
+
+ Sidebar.prototype.setCollapseAfterUpdate = function($block) {
+ $block.addClass('collapse-after-update');
+ return $('.page-with-sidebar').addClass('with-overlay');
+ };
+
+ Sidebar.prototype.onSidebarDropdownHidden = function(e) {
+ var $block, sidebar;
+ sidebar = e.data;
+ e.preventDefault();
+ $block = $(this).closest('.block');
+ return sidebar.sidebarDropdownHidden($block);
+ };
+
+ Sidebar.prototype.sidebarDropdownHidden = function($block) {
+ if ($block.hasClass('collapse-after-update')) {
+ $block.removeClass('collapse-after-update');
+ $('.page-with-sidebar').removeClass('with-overlay');
+ return this.toggleSidebar('hide');
+ }
+ };
+
+ Sidebar.prototype.triggerOpenSidebar = function() {
+ return this.sidebar.find('.js-sidebar-toggle').trigger('click');
+ };
+
+ Sidebar.prototype.toggleSidebar = function(action) {
+ if (action == null) {
+ action = 'toggle';
+ }
+ if (action === 'toggle') {
+ this.triggerOpenSidebar();
+ }
+ if (action === 'open') {
+ if (!this.isOpen()) {
+ this.triggerOpenSidebar();
+ }
+ }
+ if (action === 'hide') {
+ if (this.isOpen()) {
+ return this.triggerOpenSidebar();
+ }
+ }
+ };
+
+ Sidebar.prototype.isOpen = function() {
+ return this.sidebar.is('.right-sidebar-expanded');
+ };
+
+ Sidebar.prototype.getBlock = function(name) {
+ return this.sidebar.find(".block." + name);
+ };
+
+ return Sidebar;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
deleted file mode 100644
index 12340bbce54..00000000000
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ /dev/null
@@ -1,172 +0,0 @@
-class @Sidebar
- constructor: (currentUser) ->
- @sidebar = $('aside')
-
- @addEventListeners()
-
- addEventListeners: ->
- @sidebar.on('click', '.sidebar-collapsed-icon', @, @sidebarCollapseClicked)
- $('.dropdown').on('hidden.gl.dropdown', @, @onSidebarDropdownHidden)
- $('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
- $('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
-
-
- $(document)
- .off 'click', '.js-sidebar-toggle'
- .on 'click', '.js-sidebar-toggle', (e, triggered) ->
- e.preventDefault()
- $this = $(this)
- $thisIcon = $this.find 'i'
- $allGutterToggleIcons = $('.js-sidebar-toggle i')
- if $thisIcon.hasClass('fa-angle-double-right')
- $allGutterToggleIcons
- .removeClass('fa-angle-double-right')
- .addClass('fa-angle-double-left')
- $('aside.right-sidebar')
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed')
- $('.page-with-sidebar')
- .removeClass('right-sidebar-expanded')
- .addClass('right-sidebar-collapsed')
- else
- $allGutterToggleIcons
- .removeClass('fa-angle-double-left')
- .addClass('fa-angle-double-right')
- $('aside.right-sidebar')
- .removeClass('right-sidebar-collapsed')
- .addClass('right-sidebar-expanded')
- $('.page-with-sidebar')
- .removeClass('right-sidebar-collapsed')
- .addClass('right-sidebar-expanded')
- if not triggered
- $.cookie("collapsed_gutter",
- $('.right-sidebar')
- .hasClass('right-sidebar-collapsed'), { path: '/' })
-
- $(document)
- .off 'click', '.js-issuable-todo'
- .on 'click', '.js-issuable-todo', @toggleTodo
-
- toggleTodo: (e) =>
- $this = $(e.currentTarget)
- $todoLoading = $('.js-issuable-todo-loading')
- $btnText = $('.js-issuable-todo-text', $this)
- ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
-
- if $this.attr('data-delete-path')
- url = "#{$this.attr('data-delete-path')}"
- else
- url = "#{$this.data('url')}"
-
- $.ajax(
- url: url
- type: ajaxType
- dataType: 'json'
- data:
- issuable_id: $this.data('issuable-id')
- issuable_type: $this.data('issuable-type')
- beforeSend: =>
- @beforeTodoSend($this, $todoLoading)
- ).done (data) =>
- @todoUpdateDone(data, $this, $btnText, $todoLoading)
-
- beforeTodoSend: ($btn, $todoLoading) ->
- $btn.disable()
- $todoLoading.removeClass 'hidden'
-
- todoUpdateDone: (data, $btn, $btnText, $todoLoading) ->
- $todoPendingCount = $('.todos-pending-count')
- $todoPendingCount.text data.count
-
- $btn.enable()
- $todoLoading.addClass 'hidden'
-
- if data.count is 0
- $todoPendingCount.addClass 'hidden'
- else
- $todoPendingCount.removeClass 'hidden'
-
- if data.delete_path?
- $btn
- .attr 'aria-label', $btn.data('mark-text')
- .attr 'data-delete-path', data.delete_path
- $btnText.text $btn.data('mark-text')
- else
- $btn
- .attr 'aria-label', $btn.data('todo-text')
- .removeAttr 'data-delete-path'
- $btnText.text $btn.data('todo-text')
-
- sidebarDropdownLoading: (e) ->
- $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
- img = $sidebarCollapsedIcon.find('img')
- i = $sidebarCollapsedIcon.find('i')
- $loading = $('<i class="fa fa-spinner fa-spin"></i>')
- if img.length
- img.before($loading)
- img.hide()
- else if i.length
- i.before($loading)
- i.hide()
-
- sidebarDropdownLoaded: (e) ->
- $sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
- img = $sidebarCollapsedIcon.find('img')
- $sidebarCollapsedIcon.find('i.fa-spin').remove()
- i = $sidebarCollapsedIcon.find('i')
- if img.length
- img.show()
- else
- i.show()
-
- sidebarCollapseClicked: (e) ->
- sidebar = e.data
- e.preventDefault()
- $block = $(@).closest('.block')
- sidebar.openDropdown($block);
-
- openDropdown: (blockOrName) ->
- $block = if _.isString(blockOrName) then @getBlock(blockOrName) else blockOrName
-
- $block.find('.edit-link').trigger('click')
-
- if not @isOpen()
- @setCollapseAfterUpdate($block)
- @toggleSidebar('open')
-
- setCollapseAfterUpdate: ($block) ->
- $block.addClass('collapse-after-update')
- $('.page-with-sidebar').addClass('with-overlay')
-
- onSidebarDropdownHidden: (e) ->
- sidebar = e.data
- e.preventDefault()
- $block = $(@).closest('.block')
- sidebar.sidebarDropdownHidden($block)
-
- sidebarDropdownHidden: ($block) ->
- if $block.hasClass('collapse-after-update')
- $block.removeClass('collapse-after-update')
- $('.page-with-sidebar').removeClass('with-overlay')
- @toggleSidebar('hide')
-
- triggerOpenSidebar: ->
- @sidebar
- .find('.js-sidebar-toggle')
- .trigger('click')
-
- toggleSidebar: (action = 'toggle') ->
- if action is 'toggle'
- @triggerOpenSidebar()
-
- if action is 'open'
- @triggerOpenSidebar() if not @isOpen()
-
- if action is 'hide'
- @triggerOpenSidebar() if @isOpen()
-
- isOpen: ->
- @sidebar.is('.right-sidebar-expanded')
-
- getBlock: (name) ->
- @sidebar.find(".block.#{name}")
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
new file mode 100644
index 00000000000..d34346f862b
--- /dev/null
+++ b/app/assets/javascripts/search.js
@@ -0,0 +1,93 @@
+(function() {
+ this.Search = (function() {
+ function Search() {
+ var $groupDropdown, $projectDropdown;
+ $groupDropdown = $('.js-search-group-dropdown');
+ $projectDropdown = $('.js-search-project-dropdown');
+ this.eventListeners();
+ $groupDropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ fieldName: 'group_id',
+ data: function(term, callback) {
+ return Api.groups(term, null, function(data) {
+ data.unshift({
+ name: 'Any'
+ });
+ data.splice(1, 0, 'divider');
+ return callback(data);
+ });
+ },
+ id: function(obj) {
+ return obj.id;
+ },
+ text: function(obj) {
+ return obj.name;
+ },
+ toggleLabel: function(obj) {
+ return ($groupDropdown.data('default-label')) + " " + obj.name;
+ },
+ clicked: (function(_this) {
+ return function() {
+ return _this.submitSearch();
+ };
+ })(this)
+ });
+ $projectDropdown.glDropdown({
+ selectable: true,
+ filterable: true,
+ fieldName: 'project_id',
+ data: function(term, callback) {
+ return Api.projects(term, 'id', function(data) {
+ data.unshift({
+ name_with_namespace: 'Any'
+ });
+ data.splice(1, 0, 'divider');
+ return callback(data);
+ });
+ },
+ id: function(obj) {
+ return obj.id;
+ },
+ text: function(obj) {
+ return obj.name_with_namespace;
+ },
+ toggleLabel: function(obj) {
+ return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace;
+ },
+ clicked: (function(_this) {
+ return function() {
+ return _this.submitSearch();
+ };
+ })(this)
+ });
+ }
+
+ Search.prototype.eventListeners = function() {
+ $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp);
+ return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField);
+ };
+
+ Search.prototype.submitSearch = function() {
+ return $('.js-search-form').submit();
+ };
+
+ Search.prototype.searchKeyUp = function() {
+ var $input;
+ $input = $(this);
+ if ($input.val() === '') {
+ return $('.js-search-clear').addClass('hidden');
+ } else {
+ return $('.js-search-clear').removeClass('hidden');
+ }
+ };
+
+ Search.prototype.clearSearchField = function() {
+ return $('.js-search-input').val('').trigger('keyup').focus();
+ };
+
+ return Search;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/search.js.coffee b/app/assets/javascripts/search.js.coffee
deleted file mode 100644
index 661e1195f60..00000000000
--- a/app/assets/javascripts/search.js.coffee
+++ /dev/null
@@ -1,75 +0,0 @@
-class @Search
- constructor: ->
- $groupDropdown = $('.js-search-group-dropdown')
- $projectDropdown = $('.js-search-project-dropdown')
- @eventListeners()
-
- $groupDropdown.glDropdown(
- selectable: true
- filterable: true
- fieldName: 'group_id'
- data: (term, callback) ->
- Api.groups term, null, (data) ->
- data.unshift(
- name: 'Any'
- )
- data.splice 1, 0, 'divider'
-
- callback(data)
- id: (obj) ->
- obj.id
- text: (obj) ->
- obj.name
- toggleLabel: (obj) ->
- "#{$groupDropdown.data('default-label')} #{obj.name}"
- clicked: =>
- @submitSearch()
- )
-
- $projectDropdown.glDropdown(
- selectable: true
- filterable: true
- fieldName: 'project_id'
- data: (term, callback) ->
- Api.projects term, 'id', (data) ->
- data.unshift(
- name_with_namespace: 'Any'
- )
- data.splice 1, 0, 'divider'
-
- callback(data)
- id: (obj) ->
- obj.id
- text: (obj) ->
- obj.name_with_namespace
- toggleLabel: (obj) ->
- "#{$projectDropdown.data('default-label')} #{obj.name_with_namespace}"
- clicked: =>
- @submitSearch()
- )
-
- eventListeners: ->
- $(document)
- .off 'keyup', '.js-search-input'
- .on 'keyup', '.js-search-input', @searchKeyUp
-
- $(document)
- .off 'click', '.js-search-clear'
- .on 'click', '.js-search-clear', @clearSearchField
-
- submitSearch: ->
- $('.js-search-form').submit()
-
- searchKeyUp: ->
- $input = $(@)
-
- if $input.val() is ''
- $('.js-search-clear').addClass 'hidden'
- else
- $('.js-search-clear').removeClass 'hidden'
-
- clearSearchField: ->
- $('.js-search-input')
- .val ''
- .trigger 'keyup'
- .focus()
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
new file mode 100644
index 00000000000..227e8c696b4
--- /dev/null
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -0,0 +1,370 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.SearchAutocomplete = (function() {
+ var KEYCODE;
+
+ KEYCODE = {
+ ESCAPE: 27,
+ BACKSPACE: 8,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
+ };
+
+ function SearchAutocomplete(opts) {
+ var ref, ref1, ref2, ref3, ref4;
+ if (opts == null) {
+ opts = {};
+ }
+ this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
+ this.onClearInputClick = bind(this.onClearInputClick, this);
+ this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
+ this.onSearchInputClick = bind(this.onSearchInputClick, this);
+ this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
+ this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
+ this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
+ this.dropdown = this.wrap.find('.dropdown');
+ this.dropdownContent = this.dropdown.find('.dropdown-content');
+ this.locationBadgeEl = this.getElement('.location-badge');
+ this.scopeInputEl = this.getElement('#scope');
+ this.searchInput = this.getElement('.search-input');
+ this.projectInputEl = this.getElement('#search_project_id');
+ this.groupInputEl = this.getElement('#group_id');
+ this.searchCodeInputEl = this.getElement('#search_code');
+ this.repositoryInputEl = this.getElement('#repository_ref');
+ this.clearInput = this.getElement('.js-clear-input');
+ this.saveOriginalState();
+ if (gon.current_user_id) {
+ this.createAutocomplete();
+ }
+ this.searchInput.addClass('disabled');
+ this.saveTextLength();
+ this.bindEvents();
+ }
+
+ SearchAutocomplete.prototype.getElement = function(selector) {
+ return this.wrap.find(selector);
+ };
+
+ SearchAutocomplete.prototype.saveOriginalState = function() {
+ return this.originalState = this.serializeState();
+ };
+
+ SearchAutocomplete.prototype.saveTextLength = function() {
+ return this.lastTextLength = this.searchInput.val().length;
+ };
+
+ SearchAutocomplete.prototype.createAutocomplete = function() {
+ return this.searchInput.glDropdown({
+ filterInputBlur: false,
+ filterable: true,
+ filterRemote: true,
+ highlight: true,
+ enterCallback: false,
+ filterInput: 'input#search',
+ search: {
+ fields: ['text']
+ },
+ data: this.getData.bind(this),
+ selectable: true,
+ clicked: this.onClick.bind(this)
+ });
+ };
+
+ SearchAutocomplete.prototype.getData = function(term, callback) {
+ var _this, contents, jqXHR;
+ _this = this;
+ if (!term) {
+ if (contents = this.getCategoryContents()) {
+ this.searchInput.data('glDropdown').filter.options.callback(contents);
+ this.enableAutocomplete();
+ }
+ return;
+ }
+ if (this.loadingSuggestions) {
+ return;
+ }
+ this.loadingSuggestions = true;
+ return jqXHR = $.get(this.autocompletePath, {
+ project_id: this.projectId,
+ project_ref: this.projectRef,
+ term: term
+ }, function(response) {
+ var data, firstCategory, i, lastCategory, len, suggestion;
+ if (!response.length) {
+ _this.disableAutocomplete();
+ return;
+ }
+ data = [];
+ firstCategory = true;
+ for (i = 0, len = response.length; i < len; i++) {
+ suggestion = response[i];
+ if (lastCategory !== suggestion.category) {
+ if (!firstCategory) {
+ data.push('separator');
+ }
+ if (firstCategory) {
+ firstCategory = false;
+ }
+ data.push({
+ header: suggestion.category
+ });
+ lastCategory = suggestion.category;
+ }
+ data.push({
+ id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
+ category: suggestion.category,
+ text: suggestion.label,
+ url: suggestion.url
+ });
+ }
+ if (data.length) {
+ data.push('separator');
+ data.push({
+ text: "Result name contains \"" + term + "\"",
+ url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val())
+ });
+ }
+ return callback(data);
+ }).always(function() {
+ return _this.loadingSuggestions = false;
+ });
+ };
+
+ SearchAutocomplete.prototype.getCategoryContents = function() {
+ var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
+ userId = gon.current_user_id;
+ utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
+ if (utils.isInGroupsPage() && groupOptions) {
+ options = groupOptions[utils.getGroupSlug()];
+ } else if (utils.isInProjectPage() && projectOptions) {
+ options = projectOptions[utils.getProjectSlug()];
+ } else if (dashboardOptions) {
+ options = dashboardOptions;
+ }
+ issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name;
+ items = [
+ {
+ header: "" + name
+ }, {
+ text: 'Issues assigned to me',
+ url: issuesPath + "/?assignee_id=" + userId
+ }, {
+ text: "Issues I've created",
+ url: issuesPath + "/?author_id=" + userId
+ }, 'separator', {
+ text: 'Merge requests assigned to me',
+ url: mrPath + "/?assignee_id=" + userId
+ }, {
+ text: "Merge requests I've created",
+ url: mrPath + "/?author_id=" + userId
+ }
+ ];
+ if (!name) {
+ items.splice(0, 1);
+ }
+ return items;
+ };
+
+ SearchAutocomplete.prototype.serializeState = function() {
+ return {
+ search_project_id: this.projectInputEl.val(),
+ group_id: this.groupInputEl.val(),
+ search_code: this.searchCodeInputEl.val(),
+ repository_ref: this.repositoryInputEl.val(),
+ scope: this.scopeInputEl.val(),
+ _location: this.locationBadgeEl.text()
+ };
+ };
+
+ SearchAutocomplete.prototype.bindEvents = function() {
+ this.searchInput.on('keydown', this.onSearchInputKeyDown);
+ this.searchInput.on('keyup', this.onSearchInputKeyUp);
+ this.searchInput.on('click', this.onSearchInputClick);
+ this.searchInput.on('focus', this.onSearchInputFocus);
+ this.searchInput.on('blur', this.onSearchInputBlur);
+ this.clearInput.on('click', this.onClearInputClick);
+ return this.locationBadgeEl.on('click', (function(_this) {
+ return function() {
+ return _this.searchInput.focus();
+ };
+ })(this));
+ };
+
+ SearchAutocomplete.prototype.enableAutocomplete = function() {
+ var _this;
+ if (!gon.current_user_id) {
+ return;
+ }
+ if (!this.dropdown.hasClass('open')) {
+ _this = this;
+ this.loadingSuggestions = false;
+ this.dropdown.addClass('open').trigger('shown.bs.dropdown');
+ return this.searchInput.removeClass('disabled');
+ }
+ };
+
+ SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
+ return this.saveTextLength();
+ };
+
+ SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
+ switch (e.keyCode) {
+ case KEYCODE.BACKSPACE:
+ if (this.lastTextLength === 0 && this.badgePresent()) {
+ this.removeLocationBadge();
+ }
+ if (this.lastTextLength === 1) {
+ this.disableAutocomplete();
+ }
+ if (this.lastTextLength > 1) {
+ this.enableAutocomplete();
+ }
+ break;
+ case KEYCODE.ESCAPE:
+ this.restoreOriginalState();
+ break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
+ default:
+ if (this.searchInput.val() === '') {
+ this.disableAutocomplete();
+ } else {
+ if (e.keyCode !== KEYCODE.ENTER) {
+ this.enableAutocomplete();
+ }
+ }
+ }
+ this.wrap.toggleClass('has-value', !!e.target.value);
+ };
+
+ SearchAutocomplete.prototype.onSearchInputClick = function(e) {
+ return e.stopImmediatePropagation();
+ };
+
+ SearchAutocomplete.prototype.onSearchInputFocus = function() {
+ this.isFocused = true;
+ this.wrap.addClass('search-active');
+ if (this.getValue() === '') {
+ return this.getData();
+ }
+ };
+
+ SearchAutocomplete.prototype.getValue = function() {
+ return this.searchInput.val();
+ };
+
+ SearchAutocomplete.prototype.onClearInputClick = function(e) {
+ e.preventDefault();
+ return this.searchInput.val('').focus();
+ };
+
+ SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
+ this.isFocused = false;
+ this.wrap.removeClass('search-active');
+ if (this.searchInput.val() === '') {
+ return this.restoreOriginalState();
+ }
+ };
+
+ SearchAutocomplete.prototype.addLocationBadge = function(item) {
+ var badgeText, category, value;
+ category = item.category != null ? item.category + ": " : '';
+ value = item.value != null ? item.value : '';
+ badgeText = "" + category + value;
+ this.locationBadgeEl.text(badgeText).show();
+ return this.wrap.addClass('has-location-badge');
+ };
+
+ SearchAutocomplete.prototype.hasLocationBadge = function() {
+ return this.wrap.is('.has-location-badge');
+ };
+
+ SearchAutocomplete.prototype.restoreOriginalState = function() {
+ var i, input, inputs, len;
+ inputs = Object.keys(this.originalState);
+ for (i = 0, len = inputs.length; i < len; i++) {
+ input = inputs[i];
+ this.getElement("#" + input).val(this.originalState[input]);
+ }
+ if (this.originalState._location === '') {
+ return this.locationBadgeEl.hide();
+ } else {
+ return this.addLocationBadge({
+ value: this.originalState._location
+ });
+ }
+ };
+
+ SearchAutocomplete.prototype.badgePresent = function() {
+ return this.locationBadgeEl.length;
+ };
+
+ SearchAutocomplete.prototype.resetSearchState = function() {
+ var i, input, inputs, len, results;
+ inputs = Object.keys(this.originalState);
+ results = [];
+ for (i = 0, len = inputs.length; i < len; i++) {
+ input = inputs[i];
+ if (input === '_location') {
+ break;
+ }
+ results.push(this.getElement("#" + input).val(''));
+ }
+ return results;
+ };
+
+ SearchAutocomplete.prototype.removeLocationBadge = function() {
+ this.locationBadgeEl.hide();
+ this.resetSearchState();
+ this.wrap.removeClass('has-location-badge');
+ return this.disableAutocomplete();
+ };
+
+ SearchAutocomplete.prototype.disableAutocomplete = function() {
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
+ };
+
+ SearchAutocomplete.prototype.restoreMenu = function() {
+ var html;
+ html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
+ return this.dropdownContent.html(html);
+ };
+
+ SearchAutocomplete.prototype.onClick = function(item, $el, e) {
+ if (location.pathname.indexOf(item.url) !== -1) {
+ e.preventDefault();
+ if (!this.badgePresent) {
+ if (item.category === 'Projects') {
+ this.projectInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This project'
+ });
+ }
+ if (item.category === 'Groups') {
+ this.groupInputEl.val(item.id);
+ this.addLocationBadge({
+ value: 'This group'
+ });
+ }
+ }
+ $el.removeClass('is-active');
+ this.disableAutocomplete();
+ return this.searchInput.val('').focus();
+ }
+ };
+
+ return SearchAutocomplete;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/search_autocomplete.js.coffee b/app/assets/javascripts/search_autocomplete.js.coffee
deleted file mode 100644
index 72b1d3dfb1e..00000000000
--- a/app/assets/javascripts/search_autocomplete.js.coffee
+++ /dev/null
@@ -1,334 +0,0 @@
-class @SearchAutocomplete
-
- KEYCODE =
- ESCAPE: 27
- BACKSPACE: 8
- ENTER: 13
-
- constructor: (opts = {}) ->
- {
- @wrap = $('.search')
-
- @optsEl = @wrap.find('.search-autocomplete-opts')
- @autocompletePath = @optsEl.data('autocomplete-path')
- @projectId = @optsEl.data('autocomplete-project-id') || ''
- @projectRef = @optsEl.data('autocomplete-project-ref') || ''
-
- } = opts
-
- # Dropdown Element
- @dropdown = @wrap.find('.dropdown')
- @dropdownContent = @dropdown.find('.dropdown-content')
-
- @locationBadgeEl = @getElement('.location-badge')
- @scopeInputEl = @getElement('#scope')
- @searchInput = @getElement('.search-input')
- @projectInputEl = @getElement('#search_project_id')
- @groupInputEl = @getElement('#group_id')
- @searchCodeInputEl = @getElement('#search_code')
- @repositoryInputEl = @getElement('#repository_ref')
- @clearInput = @getElement('.js-clear-input')
-
- @saveOriginalState()
-
- # Only when user is logged in
- @createAutocomplete() if gon.current_user_id
-
- @searchInput.addClass('disabled')
-
- @saveTextLength()
-
- @bindEvents()
-
- # Finds an element inside wrapper element
- getElement: (selector) ->
- @wrap.find(selector)
-
- saveOriginalState: ->
- @originalState = @serializeState()
-
- saveTextLength: ->
- @lastTextLength = @searchInput.val().length
-
- createAutocomplete: ->
- @searchInput.glDropdown
- filterInputBlur: false
- filterable: true
- filterRemote: true
- highlight: true
- enterCallback: false
- filterInput: 'input#search'
- search:
- fields: ['text']
- data: @getData.bind(@)
- selectable: true
- clicked: @onClick.bind(@)
-
- getData: (term, callback) ->
- _this = @
-
- unless term
- if contents = @getCategoryContents()
- @searchInput.data('glDropdown').filter.options.callback contents
- @enableAutocomplete()
-
- return
-
- # Prevent multiple ajax calls
- return if @loadingSuggestions
-
- @loadingSuggestions = true
-
- jqXHR = $.get(@autocompletePath, {
- project_id: @projectId
- project_ref: @projectRef
- term: term
- }, (response) ->
- # Hide dropdown menu if no suggestions returns
- if !response.length
- _this.disableAutocomplete()
- return
-
- data = []
-
- # List results
- firstCategory = true
- for suggestion in response
-
- # Add group header before list each group
- if lastCategory isnt suggestion.category
- data.push 'separator' if !firstCategory
-
- firstCategory = false if firstCategory
-
- data.push
- header: suggestion.category
-
- lastCategory = suggestion.category
-
- data.push
- id: "#{suggestion.category.toLowerCase()}-#{suggestion.id}"
- category: suggestion.category
- text: suggestion.label
- url: suggestion.url
-
- # Add option to proceed with the search
- if data.length
- data.push('separator')
- data.push
- text: "Result name contains \"#{term}\""
- url: "/search?\
- search=#{term}\
- &project_id=#{_this.projectInputEl.val()}\
- &group_id=#{_this.groupInputEl.val()}"
-
- callback(data)
- ).always ->
- _this.loadingSuggestions = false
-
-
- getCategoryContents: ->
-
- userId = gon.current_user_id
- { utils, projectOptions, groupOptions, dashboardOptions } = gl
-
- if utils.isInGroupsPage() and groupOptions
- options = groupOptions[utils.getGroupSlug()]
-
- else if utils.isInProjectPage() and projectOptions
- options = projectOptions[utils.getProjectSlug()]
-
- else if dashboardOptions
- options = dashboardOptions
-
- { issuesPath, mrPath, name } = options
-
- items = [
- { header: "#{name}" }
- { text: 'Issues assigned to me', url: "#{issuesPath}/?assignee_id=#{userId}" }
- { text: "Issues I've created", url: "#{issuesPath}/?author_id=#{userId}" }
- 'separator'
- { text: 'Merge requests assigned to me', url: "#{mrPath}/?assignee_id=#{userId}" }
- { text: "Merge requests I've created", url: "#{mrPath}/?author_id=#{userId}" }
- ]
-
- items.splice 0, 1 unless name
-
- return items
-
-
- serializeState: ->
- {
- # Search Criteria
- search_project_id: @projectInputEl.val()
- group_id: @groupInputEl.val()
- search_code: @searchCodeInputEl.val()
- repository_ref: @repositoryInputEl.val()
- scope: @scopeInputEl.val()
-
- # Location badge
- _location: @locationBadgeEl.text()
- }
-
- bindEvents: ->
- @searchInput.on 'keydown', @onSearchInputKeyDown
- @searchInput.on 'keyup', @onSearchInputKeyUp
- @searchInput.on 'click', @onSearchInputClick
- @searchInput.on 'focus', @onSearchInputFocus
- @searchInput.on 'blur', @onSearchInputBlur
- @clearInput.on 'click', @onClearInputClick
- @locationBadgeEl.on 'click', =>
- @searchInput.focus()
-
- enableAutocomplete: ->
- # No need to enable anything if user is not logged in
- return if !gon.current_user_id
-
- unless @dropdown.hasClass('open')
- _this = @
- @loadingSuggestions = false
-
- @dropdown
- .addClass('open')
- .trigger('shown.bs.dropdown')
- @searchInput.removeClass('disabled')
-
- onSearchInputKeyDown: =>
- # Saves last length of the entered text
- @saveTextLength()
-
- onSearchInputKeyUp: (e) =>
- switch e.keyCode
- when KEYCODE.BACKSPACE
- # when trying to remove the location badge
- if @lastTextLength is 0 and @badgePresent()
- @removeLocationBadge()
-
- # When removing the last character and no badge is present
- if @lastTextLength is 1
- @disableAutocomplete()
-
- # When removing any character from existin value
- if @lastTextLength > 1
- @enableAutocomplete()
-
- when KEYCODE.ESCAPE
- @restoreOriginalState()
-
- else
- # Handle the case when deleting the input value other than backspace
- # e.g. Pressing ctrl + backspace or ctrl + x
- if @searchInput.val() is ''
- @disableAutocomplete()
- else
- # We should display the menu only when input is not empty
- @enableAutocomplete() if e.keyCode isnt KEYCODE.ENTER
-
- @wrap.toggleClass 'has-value', !!e.target.value
-
- # Avoid falsy value to be returned
- return
-
- onSearchInputClick: (e) =>
- # Prevents closing the dropdown menu
- e.stopImmediatePropagation()
-
- onSearchInputFocus: =>
- @isFocused = true
- @wrap.addClass('search-active')
-
- @getData() if @getValue() is ''
-
-
- getValue: -> return @searchInput.val()
-
-
- onClearInputClick: (e) =>
- e.preventDefault()
- @searchInput.val('').focus()
-
- onSearchInputBlur: (e) =>
- @isFocused = false
- @wrap.removeClass('search-active')
-
- # If input is blank then restore state
- if @searchInput.val() is ''
- @restoreOriginalState()
-
- addLocationBadge: (item) ->
- category = if item.category? then "#{item.category}: " else ''
- value = if item.value? then item.value else ''
-
- badgeText = "#{category}#{value}"
- @locationBadgeEl.text(badgeText).show()
- @wrap.addClass('has-location-badge')
-
-
- hasLocationBadge: -> return @wrap.is '.has-location-badge'
-
-
- restoreOriginalState: ->
- inputs = Object.keys @originalState
-
- for input in inputs
- @getElement("##{input}").val(@originalState[input])
-
- if @originalState._location is ''
- @locationBadgeEl.hide()
- else
- @addLocationBadge(
- value: @originalState._location
- )
-
- badgePresent: ->
- @locationBadgeEl.length
-
- resetSearchState: ->
- inputs = Object.keys @originalState
-
- for input in inputs
-
- # _location isnt a input
- break if input is '_location'
-
- @getElement("##{input}").val('')
-
-
- removeLocationBadge: ->
-
- @locationBadgeEl.hide()
- @resetSearchState()
- @wrap.removeClass('has-location-badge')
- @disableAutocomplete()
-
-
- disableAutocomplete: ->
- @searchInput.addClass('disabled')
- @dropdown.removeClass('open')
- @restoreMenu()
-
- restoreMenu: ->
- html = "<ul>
- <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li>
- </ul>"
- @dropdownContent.html(html)
-
- onClick: (item, $el, e) ->
- if location.pathname.indexOf(item.url) isnt -1
- e.preventDefault()
- if not @badgePresent
- if item.category is 'Projects'
- @projectInputEl.val(item.id)
- @addLocationBadge(
- value: 'This project'
- )
-
- if item.category is 'Groups'
- @groupInputEl.val(item.id)
- @addLocationBadge(
- value: 'This group'
- )
-
- $el.removeClass('is-active')
- @disableAutocomplete()
- @searchInput.val('').focus()
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
new file mode 100644
index 00000000000..3b28332854a
--- /dev/null
+++ b/app/assets/javascripts/shortcuts.js
@@ -0,0 +1,97 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Shortcuts = (function() {
+ function Shortcuts(skipResetBindings) {
+ this.onToggleHelp = bind(this.onToggleHelp, this);
+ this.enabledHelp = [];
+ if (!skipResetBindings) {
+ Mousetrap.reset();
+ }
+ Mousetrap.bind('?', this.onToggleHelp);
+ Mousetrap.bind('s', Shortcuts.focusSearch);
+ Mousetrap.bind('f', (function(_this) {
+ return function(e) {
+ return _this.focusFilter(e);
+ };
+ })(this));
+ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
+ if (typeof findFileURL !== "undefined" && findFileURL !== null) {
+ Mousetrap.bind('t', function() {
+ return Turbolinks.visit(findFileURL);
+ });
+ }
+ }
+
+ Shortcuts.prototype.onToggleHelp = function(e) {
+ e.preventDefault();
+ return Shortcuts.toggleHelp(this.enabledHelp);
+ };
+
+ Shortcuts.prototype.toggleMarkdownPreview = function(e) {
+ return $(document).triggerHandler('markdown-preview:toggle', [e]);
+ };
+
+ Shortcuts.toggleHelp = function(location) {
+ var $modal;
+ $modal = $('#modal-shortcuts');
+ if ($modal.length) {
+ $modal.modal('toggle');
+ return;
+ }
+ return $.ajax({
+ url: gon.shortcuts_path,
+ dataType: 'script',
+ success: function(e) {
+ var i, l, len, results;
+ if (location && location.length > 0) {
+ results = [];
+ for (i = 0, len = location.length; i < len; i++) {
+ l = location[i];
+ results.push($(l).show());
+ }
+ return results;
+ } else {
+ $('.hidden-shortcut').show();
+ return $('.js-more-help-button').remove();
+ }
+ }
+ });
+ };
+
+ Shortcuts.prototype.focusFilter = function(e) {
+ if (this.filterInput == null) {
+ this.filterInput = $('input[type=search]', '.nav-controls');
+ }
+ this.filterInput.focus();
+ return e.preventDefault();
+ };
+
+ Shortcuts.focusSearch = function(e) {
+ $('#search').focus();
+ return e.preventDefault();
+ };
+
+ return Shortcuts;
+
+ })();
+
+ $(document).on('click.more_help', '.js-more-help-button', function(e) {
+ $(this).remove();
+ $('.hidden-shortcut').show();
+ return e.preventDefault();
+ });
+
+ Mousetrap.stopCallback = (function() {
+ var defaultStopCallback;
+ defaultStopCallback = Mousetrap.stopCallback;
+ return function(e, element, combo) {
+ if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
+ return false;
+ } else {
+ return defaultStopCallback.apply(this, arguments);
+ }
+ };
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
deleted file mode 100644
index 3319a67a79d..00000000000
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ /dev/null
@@ -1,53 +0,0 @@
-class @Shortcuts
- constructor: (skipResetBindings) ->
- @enabledHelp = []
- Mousetrap.reset() if not skipResetBindings
- Mousetrap.bind('?', @onToggleHelp)
- Mousetrap.bind('s', Shortcuts.focusSearch)
- Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], @toggleMarkdownPreview)
- Mousetrap.bind('t', -> Turbolinks.visit(findFileURL)) if findFileURL?
-
- onToggleHelp: (e) =>
- e.preventDefault()
- Shortcuts.toggleHelp(@enabledHelp)
-
- toggleMarkdownPreview: (e) ->
- $(document).triggerHandler('markdown-preview:toggle', [e])
-
- @toggleHelp: (location) ->
- $modal = $('#modal-shortcuts')
-
- if $modal.length
- $modal.modal('toggle')
- return
-
- $.ajax(
- url: gon.shortcuts_path,
- dataType: 'script',
- success: (e) ->
- if location and location.length > 0
- $(l).show() for l in location
- else
- $('.hidden-shortcut').show()
- $('.js-more-help-button').remove()
- )
-
- @focusSearch: (e) ->
- $('#search').focus()
- e.preventDefault()
-
-$(document).on 'click.more_help', '.js-more-help-button', (e) ->
- $(@).remove()
- $('.hidden-shortcut').show()
- e.preventDefault()
-
-Mousetrap.stopCallback = (->
- defaultStopCallback = Mousetrap.stopCallback
-
- return (e, element, combo) ->
- # allowed shortcuts if textarea, input, contenteditable are focused
- if ['ctrl+shift+p', 'command+shift+p'].indexOf(combo) != -1
- return false
- else
- return defaultStopCallback.apply(@, arguments)
-)()
diff --git a/app/assets/javascripts/shortcuts_blob.coffee b/app/assets/javascripts/shortcuts_blob.coffee
deleted file mode 100644
index 6d21e5d1150..00000000000
--- a/app/assets/javascripts/shortcuts_blob.coffee
+++ /dev/null
@@ -1,10 +0,0 @@
-#= require shortcuts
-
-class @ShortcutsBlob extends Shortcuts
- constructor: (skipResetBindings) ->
- super skipResetBindings
- Mousetrap.bind('y', ShortcutsBlob.copyToClipboard)
-
- @copyToClipboard: ->
- clipboardButton = $('.btn-clipboard')
- clipboardButton.click() if clipboardButton
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
new file mode 100644
index 00000000000..b931eab638f
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -0,0 +1,28 @@
+
+/*= require shortcuts */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsBlob = (function(superClass) {
+ extend(ShortcutsBlob, superClass);
+
+ function ShortcutsBlob(skipResetBindings) {
+ ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
+ Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
+ }
+
+ ShortcutsBlob.copyToClipboard = function() {
+ var clipboardButton;
+ clipboardButton = $('.btn-clipboard');
+ if (clipboardButton) {
+ return clipboardButton.click();
+ }
+ };
+
+ return ShortcutsBlob;
+
+ })(Shortcuts);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
new file mode 100644
index 00000000000..f7492a2aa5c
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -0,0 +1,39 @@
+
+/*= require shortcuts */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsDashboardNavigation = (function(superClass) {
+ extend(ShortcutsDashboardNavigation, superClass);
+
+ function ShortcutsDashboardNavigation() {
+ ShortcutsDashboardNavigation.__super__.constructor.call(this);
+ Mousetrap.bind('g a', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity');
+ });
+ Mousetrap.bind('g i', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues');
+ });
+ Mousetrap.bind('g m', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests');
+ });
+ Mousetrap.bind('g p', function() {
+ return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects');
+ });
+ }
+
+ ShortcutsDashboardNavigation.findAndFollowLink = function(selector) {
+ var link;
+ link = $(selector).attr('href');
+ if (link) {
+ return window.location = link;
+ }
+ };
+
+ return ShortcutsDashboardNavigation;
+
+ })(Shortcuts);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
deleted file mode 100644
index cca2b8a1fcc..00000000000
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-#= require shortcuts
-
-class @ShortcutsDashboardNavigation extends Shortcuts
- constructor: ->
- super()
- Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity'))
- Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues'))
- Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests'))
- Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects'))
-
- @findAndFollowLink: (selector) ->
- link = $(selector).attr('href')
- if link
- window.location = link
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
new file mode 100644
index 00000000000..6c78914d338
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -0,0 +1,35 @@
+
+/*= require shortcuts_navigation */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsFindFile = (function(superClass) {
+ extend(ShortcutsFindFile, superClass);
+
+ function ShortcutsFindFile(projectFindFile) {
+ var _oldStopCallback;
+ this.projectFindFile = projectFindFile;
+ ShortcutsFindFile.__super__.constructor.call(this);
+ _oldStopCallback = Mousetrap.stopCallback;
+ Mousetrap.stopCallback = (function(_this) {
+ return function(event, element, combo) {
+ if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
+ event.preventDefault();
+ return false;
+ }
+ return _oldStopCallback(event, element, combo);
+ };
+ })(this);
+ Mousetrap.bind('up', this.projectFindFile.selectRowUp);
+ Mousetrap.bind('down', this.projectFindFile.selectRowDown);
+ Mousetrap.bind('esc', this.projectFindFile.goToTree);
+ Mousetrap.bind('enter', this.projectFindFile.goToBlob);
+ }
+
+ return ShortcutsFindFile;
+
+ })(ShortcutsNavigation);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee
deleted file mode 100644
index 311e80bae19..00000000000
--- a/app/assets/javascripts/shortcuts_find_file.js.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-#= require shortcuts_navigation
-
-class @ShortcutsFindFile extends ShortcutsNavigation
- constructor: (@projectFindFile) ->
- super()
- _oldStopCallback = Mousetrap.stopCallback
- # override to fire shortcuts action when focus in textbox
- Mousetrap.stopCallback = (event, element, combo) =>
- if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter')
- # when press up/down key in textbox, cusor prevent to move to home/end
- event.preventDefault()
- return false
-
- return _oldStopCallback(event, element, combo)
-
- Mousetrap.bind('up', @projectFindFile.selectRowUp)
- Mousetrap.bind('down', @projectFindFile.selectRowDown)
- Mousetrap.bind('esc', @projectFindFile.goToTree)
- Mousetrap.bind('enter', @projectFindFile.goToBlob)
diff --git a/app/assets/javascripts/shortcuts_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
deleted file mode 100644
index c93bcf3ceec..00000000000
--- a/app/assets/javascripts/shortcuts_issuable.coffee
+++ /dev/null
@@ -1,53 +0,0 @@
-#= require mousetrap
-#= require shortcuts_navigation
-
-class @ShortcutsIssuable extends ShortcutsNavigation
- constructor: (isMergeRequest) ->
- super()
- Mousetrap.bind('a', @openSidebarDropdown.bind(@, 'assignee'))
- Mousetrap.bind('m', @openSidebarDropdown.bind(@, 'milestone'))
- Mousetrap.bind('r', =>
- @replyWithSelectedText()
- return false
- )
- Mousetrap.bind('e', =>
- @editIssue()
- return false
- )
- Mousetrap.bind('l', @openSidebarDropdown.bind(@, 'labels'))
-
- if isMergeRequest
- @enabledHelp.push('.hidden-shortcut.merge_requests')
- else
- @enabledHelp.push('.hidden-shortcut.issues')
-
- replyWithSelectedText: ->
- if window.getSelection
- selected = window.getSelection().toString()
- replyField = $('.js-main-target-form #note_note')
-
- return if selected.trim() == ""
-
- # Put a '>' character before each non-empty line in the selection
- quote = _.map selected.split("\n"), (val) ->
- "> #{val}\n" if val.trim() != ''
-
- # If replyField already has some content, add a newline before our quote
- separator = replyField.val().trim() != "" and "\n" or ''
-
- replyField.val (_, current) ->
- current + separator + quote.join('') + "\n"
-
- # Trigger autosave for the added text
- replyField.trigger('input')
-
- # Focus the input field
- replyField.focus()
-
- editIssue: ->
- $editBtn = $('.issuable-edit')
- Turbolinks.visit($editBtn.attr('href'))
-
- openSidebarDropdown: (name) ->
- sidebar.openDropdown(name)
- return false
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
new file mode 100644
index 00000000000..3f3a8a9dfd9
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -0,0 +1,75 @@
+
+/*= require mousetrap */
+
+
+/*= require shortcuts_navigation */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsIssuable = (function(superClass) {
+ extend(ShortcutsIssuable, superClass);
+
+ function ShortcutsIssuable(isMergeRequest) {
+ ShortcutsIssuable.__super__.constructor.call(this);
+ Mousetrap.bind('a', this.openSidebarDropdown.bind(this, 'assignee'));
+ Mousetrap.bind('m', this.openSidebarDropdown.bind(this, 'milestone'));
+ Mousetrap.bind('r', (function(_this) {
+ return function() {
+ _this.replyWithSelectedText();
+ return false;
+ };
+ })(this));
+ Mousetrap.bind('e', (function(_this) {
+ return function() {
+ _this.editIssue();
+ return false;
+ };
+ })(this));
+ Mousetrap.bind('l', this.openSidebarDropdown.bind(this, 'labels'));
+ if (isMergeRequest) {
+ this.enabledHelp.push('.hidden-shortcut.merge_requests');
+ } else {
+ this.enabledHelp.push('.hidden-shortcut.issues');
+ }
+ }
+
+ ShortcutsIssuable.prototype.replyWithSelectedText = function() {
+ var quote, replyField, selected, separator;
+ if (window.getSelection) {
+ selected = window.getSelection().toString();
+ replyField = $('.js-main-target-form #note_note');
+ if (selected.trim() === "") {
+ return;
+ }
+ quote = _.map(selected.split("\n"), function(val) {
+ if (val.trim() !== '') {
+ return "> " + val + "\n";
+ }
+ });
+ separator = replyField.val().trim() !== "" && "\n" || '';
+ replyField.val(function(_, current) {
+ return current + separator + quote.join('') + "\n";
+ });
+ replyField.trigger('input');
+ return replyField.focus();
+ }
+ };
+
+ ShortcutsIssuable.prototype.editIssue = function() {
+ var $editBtn;
+ $editBtn = $('.issuable-edit');
+ return Turbolinks.visit($editBtn.attr('href'));
+ };
+
+ ShortcutsIssuable.prototype.openSidebarDropdown = function(name) {
+ sidebar.openDropdown(name);
+ return false;
+ };
+
+ return ShortcutsIssuable;
+
+ })(ShortcutsNavigation);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee
deleted file mode 100644
index f39504e0645..00000000000
--- a/app/assets/javascripts/shortcuts_navigation.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-#= require shortcuts
-
-class @ShortcutsNavigation extends Shortcuts
- constructor: ->
- super()
- Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project'))
- Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'))
- Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'))
- Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'))
- Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'))
- Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network'))
- Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'))
- Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'))
- Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'))
- Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'))
- Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets'))
- Mousetrap.bind('i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue'))
- @enabledHelp.push('.hidden-shortcut.project')
-
- @findAndFollowLink: (selector) ->
- link = $(selector).attr('href')
- if link
- window.location = link
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
new file mode 100644
index 00000000000..469e25482bb
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -0,0 +1,64 @@
+
+/*= require shortcuts */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsNavigation = (function(superClass) {
+ extend(ShortcutsNavigation, superClass);
+
+ function ShortcutsNavigation() {
+ ShortcutsNavigation.__super__.constructor.call(this);
+ Mousetrap.bind('g p', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-project');
+ });
+ Mousetrap.bind('g e', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity');
+ });
+ Mousetrap.bind('g f', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree');
+ });
+ Mousetrap.bind('g c', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits');
+ });
+ Mousetrap.bind('g b', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds');
+ });
+ Mousetrap.bind('g n', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
+ });
+ Mousetrap.bind('g g', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs');
+ });
+ Mousetrap.bind('g i', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
+ });
+ Mousetrap.bind('g m', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
+ });
+ Mousetrap.bind('g w', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki');
+ });
+ Mousetrap.bind('g s', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets');
+ });
+ Mousetrap.bind('i', function() {
+ return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue');
+ });
+ this.enabledHelp.push('.hidden-shortcut.project');
+ }
+
+ ShortcutsNavigation.findAndFollowLink = function(selector) {
+ var link;
+ link = $(selector).attr('href');
+ if (link) {
+ return window.location = link;
+ }
+ };
+
+ return ShortcutsNavigation;
+
+ })(Shortcuts);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
new file mode 100644
index 00000000000..fb2b39e757e
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -0,0 +1,27 @@
+
+/*= require shortcuts_navigation */
+
+(function() {
+ var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
+ hasProp = {}.hasOwnProperty;
+
+ this.ShortcutsNetwork = (function(superClass) {
+ extend(ShortcutsNetwork, superClass);
+
+ function ShortcutsNetwork(graph) {
+ this.graph = graph;
+ ShortcutsNetwork.__super__.constructor.call(this);
+ Mousetrap.bind(['left', 'h'], this.graph.scrollLeft);
+ Mousetrap.bind(['right', 'l'], this.graph.scrollRight);
+ Mousetrap.bind(['up', 'k'], this.graph.scrollUp);
+ Mousetrap.bind(['down', 'j'], this.graph.scrollDown);
+ Mousetrap.bind(['shift+up', 'shift+k'], this.graph.scrollTop);
+ Mousetrap.bind(['shift+down', 'shift+j'], this.graph.scrollBottom);
+ this.enabledHelp.push('.hidden-shortcut.network');
+ }
+
+ return ShortcutsNetwork;
+
+ })(ShortcutsNavigation);
+
+}).call(this);
diff --git a/app/assets/javascripts/shortcuts_network.js.coffee b/app/assets/javascripts/shortcuts_network.js.coffee
deleted file mode 100644
index cc95ad7ebfe..00000000000
--- a/app/assets/javascripts/shortcuts_network.js.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-#= require shortcuts_navigation
-
-class @ShortcutsNetwork extends ShortcutsNavigation
- constructor: (@graph) ->
- super()
- Mousetrap.bind(['left', 'h'], @graph.scrollLeft)
- Mousetrap.bind(['right', 'l'], @graph.scrollRight)
- Mousetrap.bind(['up', 'k'], @graph.scrollUp)
- Mousetrap.bind(['down', 'j'], @graph.scrollDown)
- Mousetrap.bind(['shift+up', 'shift+k'], @graph.scrollTop)
- Mousetrap.bind(['shift+down', 'shift+j'], @graph.scrollBottom)
- @enabledHelp.push('.hidden-shortcut.network')
diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js
new file mode 100644
index 00000000000..bd0c1194b36
--- /dev/null
+++ b/app/assets/javascripts/sidebar.js
@@ -0,0 +1,41 @@
+(function() {
+ var collapsed, expanded, toggleSidebar;
+
+ collapsed = 'page-sidebar-collapsed';
+
+ expanded = 'page-sidebar-expanded';
+
+ toggleSidebar = function() {
+ $('.page-with-sidebar').toggleClass(collapsed + " " + expanded);
+ $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded");
+ if ($.cookie('pin_nav') === 'true') {
+ $('.navbar-fixed-top').toggleClass('header-pinned-nav');
+ $('.page-with-sidebar').toggleClass('page-sidebar-pinned');
+ }
+ return setTimeout((function() {
+ var niceScrollBars;
+ niceScrollBars = $('.nav-sidebar').niceScroll();
+ return niceScrollBars.updateScrollBar();
+ }), 300);
+ };
+
+ $(document).off('click', 'body').on('click', 'body', function(e) {
+ var $nav, $target, $toggle, pageExpanded;
+ if ($.cookie('pin_nav') !== 'true') {
+ $target = $(e.target);
+ $nav = $target.closest('.sidebar-wrapper');
+ pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded');
+ $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle');
+ if ($nav.length === 0 && pageExpanded && $toggle.length === 0) {
+ $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
+ return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded');
+ }
+ }
+ });
+
+ $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) {
+ e.preventDefault();
+ return toggleSidebar();
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
deleted file mode 100644
index 68009e58645..00000000000
--- a/app/assets/javascripts/sidebar.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-collapsed = 'page-sidebar-collapsed'
-expanded = 'page-sidebar-expanded'
-
-toggleSidebar = ->
- $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
- $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded")
-
- if $.cookie('pin_nav') is 'true'
- $('.navbar-fixed-top').toggleClass('header-pinned-nav')
- $('.page-with-sidebar').toggleClass('page-sidebar-pinned')
-
- setTimeout ( ->
- niceScrollBars = $('.nav-sidebar').niceScroll();
- niceScrollBars.updateScrollBar();
- ), 300
-
-$(document)
- .off 'click', 'body'
- .on 'click', 'body', (e) ->
- unless $.cookie('pin_nav') is 'true'
- $target = $(e.target)
- $nav = $target.closest('.sidebar-wrapper')
- pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded')
- $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle')
-
- if $nav.length is 0 and pageExpanded and $toggle.length is 0
- $('.page-with-sidebar')
- .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
-
- $('.navbar-fixed-top')
- .toggleClass('header-collapsed header-expanded')
-
-$(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', (e) ->
- e.preventDefault()
-
- toggleSidebar()
-)
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
new file mode 100644
index 00000000000..156b9b8abec
--- /dev/null
+++ b/app/assets/javascripts/single_file_diff.js
@@ -0,0 +1,87 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.SingleFileDiff = (function() {
+ var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
+
+ WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
+
+ LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
+
+ ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
+
+ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
+
+ function SingleFileDiff(file) {
+ this.file = file;
+ this.toggleDiff = bind(this.toggleDiff, this);
+ this.content = $('.diff-content', this.file);
+ this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
+ this.isOpen = !this.diffForPath;
+ if (this.diffForPath) {
+ this.collapsedContent = this.content;
+ this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
+ this.content = null;
+ this.collapsedContent.after(this.loadingContent);
+ } else {
+ this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
+ this.content.after(this.collapsedContent);
+ }
+ this.collapsedContent.on('click', this.toggleDiff);
+ $('.file-title > a', this.file).on('click', this.toggleDiff);
+ }
+
+ SingleFileDiff.prototype.toggleDiff = function(e) {
+ this.isOpen = !this.isOpen;
+ if (!this.isOpen && !this.hasError) {
+ this.content.hide();
+ this.collapsedContent.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+ } else if (this.content) {
+ this.collapsedContent.hide();
+ this.content.show();
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+ } else {
+ return this.getContentHTML();
+ }
+ };
+
+ SingleFileDiff.prototype.getContentHTML = function() {
+ this.collapsedContent.hide();
+ this.loadingContent.show();
+ $.get(this.diffForPath, (function(_this) {
+ return function(data) {
+ _this.loadingContent.hide();
+ if (data.html) {
+ _this.content = $(data.html);
+ _this.content.syntaxHighlight();
+ } else {
+ _this.hasError = true;
+ _this.content = $(ERROR_HTML);
+ }
+ _this.collapsedContent.after(_this.content);
+
+ if (typeof DiffNotesApp !== 'undefined') {
+ DiffNotesApp.compileComponents();
+ }
+ };
+ })(this));
+ };
+
+ return SingleFileDiff;
+
+ })();
+
+ $.fn.singleFileDiff = function() {
+ return this.each(function() {
+ if (!$.data(this, 'singleFileDiff')) {
+ return $.data(this, 'singleFileDiff', new SingleFileDiff(this));
+ }
+ });
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 00000000000..855e97eb301
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var editor = ace.edit("editor")
+
+ $(".snippet-form-holder form").on('submit', function() {
+ $(".snippet-file-content").val(editor.getValue());
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
new file mode 100644
index 00000000000..10509313c12
--- /dev/null
+++ b/app/assets/javascripts/star.js
@@ -0,0 +1,31 @@
+(function() {
+ this.Star = (function() {
+ function Star() {
+ $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
+ var $starIcon, $starSpan, $this, toggleStar;
+ $this = $(this);
+ $starSpan = $this.find('span');
+ $starIcon = $this.find('i');
+ toggleStar = function(isStarred) {
+ $this.parent().find('.star-count').text(data.star_count);
+ if (isStarred) {
+ $starSpan.removeClass('starred').text('Star');
+ gl.utils.updateTooltipTitle($this, 'Star project');
+ $starIcon.removeClass('fa-star').addClass('fa-star-o');
+ } else {
+ $starSpan.addClass('starred').text('Unstar');
+ gl.utils.updateTooltipTitle($this, 'Unstar project');
+ $starIcon.removeClass('fa-star-o').addClass('fa-star');
+ }
+ };
+ toggleStar($starSpan.hasClass('starred'));
+ }).on('ajax:error', function(e, xhr, status, error) {
+ new Flash('Star toggle failed. Try again later.', 'alert');
+ });
+ }
+
+ return Star;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee
deleted file mode 100644
index 01b28171f72..00000000000
--- a/app/assets/javascripts/star.js.coffee
+++ /dev/null
@@ -1,24 +0,0 @@
-class @Star
- constructor: ->
- $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) ->
- $this = $(this)
- $starSpan = $this.find('span')
- $starIcon = $this.find('i')
-
- toggleStar = (isStarred) ->
- $this.parent().find('.star-count').text data.star_count
- if isStarred
- $starSpan.removeClass('starred').text 'Star'
- gl.utils.updateTooltipTitle $this, 'Star project'
- $starIcon.removeClass('fa-star').addClass 'fa-star-o'
- else
- $starSpan.addClass('starred').text 'Unstar'
- gl.utils.updateTooltipTitle $this, 'Unstar project'
- $starIcon.removeClass('fa-star-o').addClass 'fa-star'
- return
-
- toggleStar $starSpan.hasClass('starred')
- return
- ).on 'ajax:error', (e, xhr, status, error) ->
- new Flash('Star toggle failed. Try again later.', 'alert')
- return
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
new file mode 100644
index 00000000000..5e3c5983d75
--- /dev/null
+++ b/app/assets/javascripts/subscription.js
@@ -0,0 +1,41 @@
+(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;
+ $container = $(container);
+ this.url = $container.attr('data-url');
+ this.subscribe_button = $container.find('.js-subscribe-button');
+ this.subscription_status = $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');
+ return $.post(this.url, (function(_this) {
+ return function() {
+ var status;
+ btn.removeClass('disabled');
+ 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.coffee b/app/assets/javascripts/subscription.js.coffee
deleted file mode 100644
index 08d494aba9f..00000000000
--- a/app/assets/javascripts/subscription.js.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-class @Subscription
- constructor: (container) ->
- $container = $(container)
- @url = $container.attr('data-url')
- @subscribe_button = $container.find('.js-subscribe-button')
- @subscription_status = $container.find('.subscription-status')
- @subscribe_button.unbind('click').click(@toggleSubscription)
-
- toggleSubscription: (event) =>
- btn = $(event.currentTarget)
- action = btn.find('span').text()
- current_status = @subscription_status.attr('data-status')
- btn.addClass('disabled')
-
- $.post @url, =>
- btn.removeClass('disabled')
- status = if current_status == 'subscribed' then 'unsubscribed' else 'subscribed'
- @subscription_status.attr('data-status', status)
- action = if status == 'subscribed' then 'Unsubscribe' else 'Subscribe'
- btn.find('span').text(action)
- @subscription_status.find('>div').toggleClass('hidden')
-
- if btn.attr('data-original-title')
- btn.tooltip('hide')
- .attr('data-original-title', action)
- .tooltip('fixTitle')
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
new file mode 100644
index 00000000000..d6c219603d1
--- /dev/null
+++ b/app/assets/javascripts/subscription_select.js
@@ -0,0 +1,35 @@
+(function() {
+ this.SubscriptionSelect = (function() {
+ function SubscriptionSelect() {
+ $('.js-subscription-event').each(function(i, el) {
+ var fieldName;
+ fieldName = $(el).data("field-name");
+ return $(el).glDropdown({
+ selectable: true,
+ fieldName: fieldName,
+ toggleLabel: (function(_this) {
+ return function(selected, el, instance) {
+ var $item, label;
+ label = 'Subscription';
+ $item = instance.dropdown.find('.is-active');
+ if ($item.length) {
+ label = $item.text();
+ }
+ return label;
+ };
+ })(this),
+ clicked: function(item, $el, e) {
+ return e.preventDefault();
+ },
+ id: function(obj, el) {
+ return $(el).data("id");
+ }
+ });
+ });
+ }
+
+ return SubscriptionSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee
deleted file mode 100644
index 980f0232d10..00000000000
--- a/app/assets/javascripts/syntax_highlight.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-# Syntax Highlighter
-#
-# Applies a syntax highlighting color scheme CSS class to any element with the
-# `js-syntax-highlight` class
-#
-# ### Example Markup
-#
-# <div class="js-syntax-highlight"></div>
-#
-$.fn.syntaxHighlight = ->
- if $(this).hasClass('js-syntax-highlight')
- # Given the element itself, apply highlighting
- $(this).addClass(gon.user_color_scheme)
- else
- # Given a parent element, recurse to any of its applicable children
- $children = $(this).find('.js-syntax-highlight')
- $children.syntaxHighlight() if $children.length
-
-$(document).on 'ready page:load', ->
- $('.js-syntax-highlight').syntaxHighlight()
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
new file mode 100644
index 00000000000..dba62638c78
--- /dev/null
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -0,0 +1,18 @@
+(function() {
+ $.fn.syntaxHighlight = function() {
+ var $children;
+ if ($(this).hasClass('js-syntax-highlight')) {
+ return $(this).addClass(gon.user_color_scheme);
+ } else {
+ $children = $(this).find('.js-syntax-highlight');
+ if ($children.length) {
+ return $children.syntaxHighlight();
+ }
+ }
+ };
+
+ $(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
new file mode 100644
index 00000000000..c32ddf80219
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -0,0 +1,51 @@
+/*= require ../blob/template_selector */
+
+((global) => {
+ class IssuableTemplateSelector extends TemplateSelector {
+ constructor(...args) {
+ super(...args);
+ this.projectPath = this.dropdown.data('project-path');
+ this.namespacePath = this.dropdown.data('namespace-path');
+ this.issuableType = this.wrapper.data('issuable-type');
+ this.titleInput = $(`#${this.issuableType}_title`);
+
+ let initialQuery = {
+ name: this.dropdown.data('selected')
+ };
+
+ if (initialQuery.name) this.requestFile(initialQuery);
+
+ $('.reset-template', this.dropdown.parent()).on('click', () => {
+ if (this.currentTemplate) this.setInputValueToTemplateContent();
+ });
+ }
+
+ requestFile(query) {
+ this.startLoadingSpinner();
+ Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ if (err) return; // Error handled by global AJAX error handler
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ });
+ return;
+ }
+
+ setInputValueToTemplateContent() {
+ // `this.requestFileSuccess` sets the value of the description input field
+ // to the content of the template selected.
+ if (this.titleInput.val() === '') {
+ // If the title has not yet been set, focus the title input and
+ // skip focusing the description input by setting `true` as the 2nd
+ // argument to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, true);
+ this.titleInput.focus();
+ } else {
+ this.requestFileSuccess(this.currentTemplate);
+ }
+ return;
+ }
+ }
+
+ global.IssuableTemplateSelector = IssuableTemplateSelector;
+})(window);
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
new file mode 100644
index 00000000000..bd8cdde033e
--- /dev/null
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -0,0 +1,29 @@
+((global) => {
+ class IssuableTemplateSelectors {
+ constructor(opts = {}) {
+ this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
+ this.editor = opts.editor || this.initEditor();
+
+ this.$dropdowns.each((i, dropdown) => {
+ let $dropdown = $(dropdown);
+ new IssuableTemplateSelector({
+ pattern: /(\.md)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor
+ });
+ });
+ }
+
+ initEditor() {
+ let editor = $('.markdown-area');
+ // Proxy ace-editor's .setValue to jQuery's .val
+ editor.setValue = editor.val;
+ editor.getValue = editor.val;
+ return editor;
+ }
+ }
+
+ global.IssuableTemplateSelectors = IssuableTemplateSelectors;
+})(window);
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
new file mode 100644
index 00000000000..6e677fa8cc6
--- /dev/null
+++ b/app/assets/javascripts/todos.js
@@ -0,0 +1,144 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Todos = (function() {
+ function Todos(opts) {
+ var ref;
+ if (opts == null) {
+ opts = {};
+ }
+ this.allDoneClicked = bind(this.allDoneClicked, this);
+ this.doneClicked = bind(this.doneClicked, this);
+ this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
+ this.perPage = this.el.data('perPage');
+ this.clearListeners();
+ this.initBtnListeners();
+ }
+
+ Todos.prototype.clearListeners = function() {
+ $('.done-todo').off('click');
+ $('.js-todos-mark-all').off('click');
+ return $('.todo').off('click');
+ };
+
+ Todos.prototype.initBtnListeners = function() {
+ $('.done-todo').on('click', this.doneClicked);
+ $('.js-todos-mark-all').on('click', this.allDoneClicked);
+ return $('.todo').on('click', this.goToTodoUrl);
+ };
+
+ Todos.prototype.doneClicked = function(e) {
+ var $this;
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $this = $(e.currentTarget);
+ $this.disable();
+ return $.ajax({
+ type: 'POST',
+ url: $this.attr('href'),
+ dataType: 'json',
+ data: {
+ '_method': 'delete'
+ },
+ success: (function(_this) {
+ return function(data) {
+ _this.redirectIfNeeded(data.count);
+ _this.clearDone($this.closest('li'));
+ return _this.updateBadges(data);
+ };
+ })(this)
+ });
+ };
+
+ Todos.prototype.allDoneClicked = function(e) {
+ var $this;
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $this = $(e.currentTarget);
+ $this.disable();
+ return $.ajax({
+ type: 'POST',
+ url: $this.attr('href'),
+ dataType: 'json',
+ data: {
+ '_method': 'delete'
+ },
+ success: (function(_this) {
+ return function(data) {
+ $this.remove();
+ $('.js-todos-list').remove();
+ return _this.updateBadges(data);
+ };
+ })(this)
+ });
+ };
+
+ Todos.prototype.clearDone = function($row) {
+ var $ul;
+ $ul = $row.closest('ul');
+ $row.remove();
+ if (!$ul.find('li').length) {
+ return $ul.parents('.panel').remove();
+ }
+ };
+
+ Todos.prototype.updateBadges = function(data) {
+ $('.todos-pending .badge, .todos-pending-count').text(data.count);
+ return $('.todos-done .badge').text(data.done_count);
+ };
+
+ Todos.prototype.getTotalPages = function() {
+ return this.el.data('totalPages');
+ };
+
+ Todos.prototype.getCurrentPage = function() {
+ return this.el.data('currentPage');
+ };
+
+ Todos.prototype.getTodosPerPage = function() {
+ return this.el.data('perPage');
+ };
+
+ Todos.prototype.redirectIfNeeded = function(total) {
+ var currPage, currPages, newPages, pageParams, url;
+ currPages = this.getTotalPages();
+ currPage = this.getCurrentPage();
+ if (!total) {
+ location.reload();
+ return;
+ }
+ if (!currPages) {
+ return;
+ }
+ newPages = Math.ceil(total / this.getTodosPerPage());
+ url = location.href;
+ if (newPages !== currPages) {
+ if (currPages > 1 && currPage === currPages) {
+ pageParams = {
+ page: currPages - 1
+ };
+ url = gl.utils.mergeUrlParams(pageParams, url);
+ }
+ return Turbolinks.visit(url);
+ }
+ };
+
+ Todos.prototype.goToTodoUrl = function(e) {
+ var todoLink;
+ todoLink = $(this).data('url');
+ if (!todoLink) {
+ return;
+ }
+ if (e.metaKey || e.which === 2) {
+ e.preventDefault();
+ return window.open(todoLink, '_blank');
+ } else {
+ return Turbolinks.visit(todoLink);
+ }
+ };
+
+ return Todos;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
deleted file mode 100644
index 10bef96f43d..00000000000
--- a/app/assets/javascripts/todos.js.coffee
+++ /dev/null
@@ -1,110 +0,0 @@
-class @Todos
- constructor: (opts = {}) ->
- {
- @el = $('.js-todos-options')
- } = opts
-
- @perPage = @el.data('perPage')
-
- @clearListeners()
- @initBtnListeners()
-
- clearListeners: ->
- $('.done-todo').off('click')
- $('.js-todos-mark-all').off('click')
- $('.todo').off('click')
-
- initBtnListeners: ->
- $('.done-todo').on('click', @doneClicked)
- $('.js-todos-mark-all').on('click', @allDoneClicked)
- $('.todo').on('click', @goToTodoUrl)
-
- doneClicked: (e) =>
- e.preventDefault()
- e.stopImmediatePropagation()
-
- $this = $(e.currentTarget)
- $this.disable()
-
- $.ajax
- type: 'POST'
- url: $this.attr('href')
- dataType: 'json'
- data: '_method': 'delete'
- success: (data) =>
- @redirectIfNeeded data.count
- @clearDone $this.closest('li')
- @updateBadges data
-
- allDoneClicked: (e) =>
- e.preventDefault()
- e.stopImmediatePropagation()
-
- $this = $(e.currentTarget)
- $this.disable()
-
- $.ajax
- type: 'POST'
- url: $this.attr('href')
- dataType: 'json'
- data: '_method': 'delete'
- success: (data) =>
- $this.remove()
- $('.js-todos-list').remove()
- @updateBadges data
-
- clearDone: ($row) ->
- $ul = $row.closest('ul')
- $row.remove()
-
- if not $ul.find('li').length
- $ul.parents('.panel').remove()
-
- updateBadges: (data) ->
- $('.todos-pending .badge, .todos-pending-count').text data.count
- $('.todos-done .badge').text data.done_count
-
- getTotalPages: ->
- @el.data('totalPages')
-
- getCurrentPage: ->
- @el.data('currentPage')
-
- getTodosPerPage: ->
- @el.data('perPage')
-
- redirectIfNeeded: (total) ->
- currPages = @getTotalPages()
- currPage = @getCurrentPage()
-
- # Refresh if no remaining Todos
- if not total
- location.reload()
- return
-
- # Do nothing if no pagination
- return if not currPages
-
- newPages = Math.ceil(total / @getTodosPerPage())
- url = location.href # Includes query strings
-
- # If new total of pages is different than we have now
- if newPages isnt currPages
- # Redirect to previous page if there's one available
- if currPages > 1 and currPage is currPages
- pageParams =
- page: currPages - 1
- url = gl.utils.mergeUrlParams(pageParams, url)
-
- Turbolinks.visit(url)
-
- goToTodoUrl: (e)->
- todoLink = $(this).data('url')
- return unless todoLink
-
- # Allow Meta-Click or Mouse3-click to open in a new tab
- if e.metaKey or e.which is 2
- e.preventDefault()
- window.open(todoLink,'_blank')
- else
- Turbolinks.visit(todoLink)
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
new file mode 100644
index 00000000000..78e159a7ed9
--- /dev/null
+++ b/app/assets/javascripts/tree.js
@@ -0,0 +1,65 @@
+(function() {
+ this.TreeView = (function() {
+ function TreeView() {
+ this.initKeyNav();
+ $(".tree-content-holder .tree-item").on('click', function(e) {
+ var $clickedEl, path;
+ $clickedEl = $(e.target);
+ path = $('.tree-item-file-name a', this).attr('href');
+ if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
+ if (e.metaKey || e.which === 2) {
+ e.preventDefault();
+ return window.open(path, '_blank');
+ } else {
+ return Turbolinks.visit(path);
+ }
+ }
+ });
+ $('span.log_loading:first').removeClass('hide');
+ }
+
+ TreeView.prototype.initKeyNav = function() {
+ var li, liSelected;
+ li = $("tr.tree-item");
+ liSelected = null;
+ return $('body').keydown(function(e) {
+ var next, path;
+ if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) {
+ return false;
+ }
+ if (e.which === 40) {
+ if (liSelected) {
+ next = liSelected.next();
+ if (next.length > 0) {
+ liSelected.removeClass("selected");
+ liSelected = next.addClass("selected");
+ }
+ } else {
+ liSelected = li.eq(0).addClass("selected");
+ }
+ return $(liSelected).focus();
+ } else if (e.which === 38) {
+ if (liSelected) {
+ next = liSelected.prev();
+ if (next.length > 0) {
+ liSelected.removeClass("selected");
+ liSelected = next.addClass("selected");
+ }
+ } else {
+ liSelected = li.last().addClass("selected");
+ }
+ return $(liSelected).focus();
+ } else if (e.which === 13) {
+ path = $('.tree-item.selected .tree-item-file-name a').attr('href');
+ if (path) {
+ return Turbolinks.visit(path);
+ }
+ }
+ });
+ };
+
+ return TreeView;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee
deleted file mode 100644
index 83de584f2d9..00000000000
--- a/app/assets/javascripts/tree.js.coffee
+++ /dev/null
@@ -1,50 +0,0 @@
-class @TreeView
- constructor: ->
- @initKeyNav()
-
- # Code browser tree slider
- # Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
- $(".tree-content-holder .tree-item").on 'click', (e) ->
- $clickedEl = $(e.target)
- path = $('.tree-item-file-name a', this).attr('href')
-
- if not $clickedEl.is('a') and not $clickedEl.is('.str-truncated')
- if e.metaKey or e.which is 2
- e.preventDefault()
- window.open path, '_blank'
- else
- Turbolinks.visit path
-
- # Show the "Loading commit data" for only the first element
- $('span.log_loading:first').removeClass('hide')
-
- initKeyNav: ->
- li = $("tr.tree-item")
- liSelected = null
- $('body').keydown (e) ->
- if $("input:focus").length > 0 && (e.which == 38 || e.which == 40)
- return false
-
- if e.which is 40
- if liSelected
- next = liSelected.next()
- if next.length > 0
- liSelected.removeClass "selected"
- liSelected = next.addClass("selected")
- else
- liSelected = li.eq(0).addClass("selected")
-
- $(liSelected).focus()
- else if e.which is 38
- if liSelected
- next = liSelected.prev()
- if next.length > 0
- liSelected.removeClass "selected"
- liSelected = next.addClass("selected")
- else
- liSelected = li.last().addClass("selected")
-
- $(liSelected).focus()
- else if e.which is 13
- path = $('.tree-item.selected .tree-item-file-name a').attr('href')
- if path then Turbolinks.visit(path)
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
new file mode 100644
index 00000000000..9ba847fb0c2
--- /dev/null
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -0,0 +1,89 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.U2FAuthenticate = (function() {
+ function U2FAuthenticate(container, u2fParams) {
+ this.container = container;
+ this.renderNotSupported = bind(this.renderNotSupported, this);
+ this.renderAuthenticated = bind(this.renderAuthenticated, this);
+ this.renderError = bind(this.renderError, this);
+ this.renderInProgress = bind(this.renderInProgress, this);
+ this.renderSetup = bind(this.renderSetup, this);
+ this.renderTemplate = bind(this.renderTemplate, this);
+ this.authenticate = bind(this.authenticate, this);
+ this.start = bind(this.start, this);
+ this.appId = u2fParams.app_id;
+ this.challenge = u2fParams.challenge;
+ this.signRequests = u2fParams.sign_requests.map(function(request) {
+ return _(request).omit('challenge');
+ });
+ }
+
+ U2FAuthenticate.prototype.start = function() {
+ if (U2FUtil.isU2FSupported()) {
+ return this.renderSetup();
+ } else {
+ return this.renderNotSupported();
+ }
+ };
+
+ U2FAuthenticate.prototype.authenticate = function() {
+ return u2f.sign(this.appId, this.challenge, this.signRequests, (function(_this) {
+ return function(response) {
+ var error;
+ if (response.errorCode) {
+ error = new U2FError(response.errorCode);
+ return _this.renderError(error);
+ } else {
+ return _this.renderAuthenticated(JSON.stringify(response));
+ }
+ };
+ })(this), 10);
+ };
+
+ U2FAuthenticate.prototype.templates = {
+ "notSupported": "#js-authenticate-u2f-not-supported",
+ "setup": '#js-authenticate-u2f-setup',
+ "inProgress": '#js-authenticate-u2f-in-progress',
+ "error": '#js-authenticate-u2f-error',
+ "authenticated": '#js-authenticate-u2f-authenticated'
+ };
+
+ U2FAuthenticate.prototype.renderTemplate = function(name, params) {
+ var template, templateString;
+ templateString = $(this.templates[name]).html();
+ template = _.template(templateString);
+ return this.container.html(template(params));
+ };
+
+ U2FAuthenticate.prototype.renderSetup = function() {
+ this.renderTemplate('setup');
+ return this.container.find('#js-login-u2f-device').on('click', this.renderInProgress);
+ };
+
+ U2FAuthenticate.prototype.renderInProgress = function() {
+ this.renderTemplate('inProgress');
+ return this.authenticate();
+ };
+
+ U2FAuthenticate.prototype.renderError = function(error) {
+ this.renderTemplate('error', {
+ error_message: error.message()
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ };
+
+ U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
+ this.renderTemplate('authenticated');
+ return this.container.find("#js-device-response").val(deviceResponse);
+ };
+
+ U2FAuthenticate.prototype.renderNotSupported = function() {
+ return this.renderTemplate('notSupported');
+ };
+
+ return U2FAuthenticate;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/u2f/authenticate.js.coffee b/app/assets/javascripts/u2f/authenticate.js.coffee
deleted file mode 100644
index 6deb902c8de..00000000000
--- a/app/assets/javascripts/u2f/authenticate.js.coffee
+++ /dev/null
@@ -1,63 +0,0 @@
-# Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
-#
-# State Flow #1: setup -> in_progress -> authenticated -> POST to server
-# State Flow #2: setup -> in_progress -> error -> setup
-
-class @U2FAuthenticate
- constructor: (@container, u2fParams) ->
- @appId = u2fParams.app_id
- @challenges = u2fParams.challenges
- @signRequests = u2fParams.sign_requests
-
- start: () =>
- if U2FUtil.isU2FSupported()
- @renderSetup()
- else
- @renderNotSupported()
-
- authenticate: () =>
- u2f.sign(@appId, @challenges, @signRequests, (response) =>
- if response.errorCode
- error = new U2FError(response.errorCode)
- @renderError(error);
- else
- @renderAuthenticated(JSON.stringify(response))
- , 10)
-
- #############
- # Rendering #
- #############
-
- templates: {
- "notSupported": "#js-authenticate-u2f-not-supported",
- "setup": '#js-authenticate-u2f-setup',
- "inProgress": '#js-authenticate-u2f-in-progress',
- "error": '#js-authenticate-u2f-error',
- "authenticated": '#js-authenticate-u2f-authenticated'
- }
-
- renderTemplate: (name, params) =>
- templateString = $(@templates[name]).html()
- template = _.template(templateString)
- @container.html(template(params))
-
- renderSetup: () =>
- @renderTemplate('setup')
- @container.find('#js-login-u2f-device').on('click', @renderInProgress)
-
- renderInProgress: () =>
- @renderTemplate('inProgress')
- @authenticate()
-
- renderError: (error) =>
- @renderTemplate('error', {error_message: error.message()})
- @container.find('#js-u2f-try-again').on('click', @renderSetup)
-
- renderAuthenticated: (deviceResponse) =>
- @renderTemplate('authenticated')
- # Prefer to do this instead of interpolating using Underscore templates
- # because of JSON escaping issues.
- @container.find("#js-device-response").val(deviceResponse)
-
- renderNotSupported: () =>
- @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
new file mode 100644
index 00000000000..bc48c67c4f2
--- /dev/null
+++ b/app/assets/javascripts/u2f/error.js
@@ -0,0 +1,27 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.U2FError = (function() {
+ function U2FError(errorCode) {
+ this.errorCode = errorCode;
+ this.message = bind(this.message, this);
+ this.httpsDisabled = window.location.protocol !== 'https:';
+ console.error("U2F Error Code: " + this.errorCode);
+ }
+
+ U2FError.prototype.message = function() {
+ switch (false) {
+ case !(this.errorCode === u2f.ErrorCodes.BAD_REQUEST && this.httpsDisabled):
+ return "U2F only works with HTTPS-enabled websites. Contact your administrator for more details.";
+ case this.errorCode !== u2f.ErrorCodes.DEVICE_INELIGIBLE:
+ return "This device has already been registered with us.";
+ default:
+ return "There was a problem communicating with your device.";
+ }
+ };
+
+ return U2FError;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/u2f/error.js.coffee b/app/assets/javascripts/u2f/error.js.coffee
deleted file mode 100644
index 1a2fc3e757f..00000000000
--- a/app/assets/javascripts/u2f/error.js.coffee
+++ /dev/null
@@ -1,13 +0,0 @@
-class @U2FError
- constructor: (@errorCode) ->
- @httpsDisabled = (window.location.protocol isnt 'https:')
- console.error("U2F Error Code: #{@errorCode}")
-
- message: () =>
- switch
- when (@errorCode is u2f.ErrorCodes.BAD_REQUEST and @httpsDisabled)
- "U2F only works with HTTPS-enabled websites. Contact your administrator for more details."
- when @errorCode is u2f.ErrorCodes.DEVICE_INELIGIBLE
- "This device has already been registered with us."
- else
- "There was a problem communicating with your device."
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
new file mode 100644
index 00000000000..c87e0840df3
--- /dev/null
+++ b/app/assets/javascripts/u2f/register.js
@@ -0,0 +1,87 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.U2FRegister = (function() {
+ function U2FRegister(container, u2fParams) {
+ this.container = container;
+ this.renderNotSupported = bind(this.renderNotSupported, this);
+ this.renderRegistered = bind(this.renderRegistered, this);
+ this.renderError = bind(this.renderError, this);
+ this.renderInProgress = bind(this.renderInProgress, this);
+ this.renderSetup = bind(this.renderSetup, this);
+ this.renderTemplate = bind(this.renderTemplate, this);
+ this.register = bind(this.register, this);
+ this.start = bind(this.start, this);
+ this.appId = u2fParams.app_id;
+ this.registerRequests = u2fParams.register_requests;
+ this.signRequests = u2fParams.sign_requests;
+ }
+
+ U2FRegister.prototype.start = function() {
+ if (U2FUtil.isU2FSupported()) {
+ return this.renderSetup();
+ } else {
+ return this.renderNotSupported();
+ }
+ };
+
+ U2FRegister.prototype.register = function() {
+ return u2f.register(this.appId, this.registerRequests, this.signRequests, (function(_this) {
+ return function(response) {
+ var error;
+ if (response.errorCode) {
+ error = new U2FError(response.errorCode);
+ return _this.renderError(error);
+ } else {
+ return _this.renderRegistered(JSON.stringify(response));
+ }
+ };
+ })(this), 10);
+ };
+
+ U2FRegister.prototype.templates = {
+ "notSupported": "#js-register-u2f-not-supported",
+ "setup": '#js-register-u2f-setup',
+ "inProgress": '#js-register-u2f-in-progress',
+ "error": '#js-register-u2f-error',
+ "registered": '#js-register-u2f-registered'
+ };
+
+ U2FRegister.prototype.renderTemplate = function(name, params) {
+ var template, templateString;
+ templateString = $(this.templates[name]).html();
+ template = _.template(templateString);
+ return this.container.html(template(params));
+ };
+
+ U2FRegister.prototype.renderSetup = function() {
+ this.renderTemplate('setup');
+ return this.container.find('#js-setup-u2f-device').on('click', this.renderInProgress);
+ };
+
+ U2FRegister.prototype.renderInProgress = function() {
+ this.renderTemplate('inProgress');
+ return this.register();
+ };
+
+ U2FRegister.prototype.renderError = function(error) {
+ this.renderTemplate('error', {
+ error_message: error.message()
+ });
+ return this.container.find('#js-u2f-try-again').on('click', this.renderSetup);
+ };
+
+ U2FRegister.prototype.renderRegistered = function(deviceResponse) {
+ this.renderTemplate('registered');
+ return this.container.find("#js-device-response").val(deviceResponse);
+ };
+
+ U2FRegister.prototype.renderNotSupported = function() {
+ return this.renderTemplate('notSupported');
+ };
+
+ return U2FRegister;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/u2f/register.js.coffee b/app/assets/javascripts/u2f/register.js.coffee
deleted file mode 100644
index 74472cfa120..00000000000
--- a/app/assets/javascripts/u2f/register.js.coffee
+++ /dev/null
@@ -1,63 +0,0 @@
-# Register U2F (universal 2nd factor) devices for users to authenticate with.
-#
-# State Flow #1: setup -> in_progress -> registered -> POST to server
-# State Flow #2: setup -> in_progress -> error -> setup
-
-class @U2FRegister
- constructor: (@container, u2fParams) ->
- @appId = u2fParams.app_id
- @registerRequests = u2fParams.register_requests
- @signRequests = u2fParams.sign_requests
-
- start: () =>
- if U2FUtil.isU2FSupported()
- @renderSetup()
- else
- @renderNotSupported()
-
- register: () =>
- u2f.register(@appId, @registerRequests, @signRequests, (response) =>
- if response.errorCode
- error = new U2FError(response.errorCode)
- @renderError(error);
- else
- @renderRegistered(JSON.stringify(response))
- , 10)
-
- #############
- # Rendering #
- #############
-
- templates: {
- "notSupported": "#js-register-u2f-not-supported",
- "setup": '#js-register-u2f-setup',
- "inProgress": '#js-register-u2f-in-progress',
- "error": '#js-register-u2f-error',
- "registered": '#js-register-u2f-registered'
- }
-
- renderTemplate: (name, params) =>
- templateString = $(@templates[name]).html()
- template = _.template(templateString)
- @container.html(template(params))
-
- renderSetup: () =>
- @renderTemplate('setup')
- @container.find('#js-setup-u2f-device').on('click', @renderInProgress)
-
- renderInProgress: () =>
- @renderTemplate('inProgress')
- @register()
-
- renderError: (error) =>
- @renderTemplate('error', {error_message: error.message()})
- @container.find('#js-u2f-try-again').on('click', @renderSetup)
-
- renderRegistered: (deviceResponse) =>
- @renderTemplate('registered')
- # Prefer to do this instead of interpolating using Underscore templates
- # because of JSON escaping issues.
- @container.find("#js-device-response").val(deviceResponse)
-
- renderNotSupported: () =>
- @renderTemplate('notSupported')
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
new file mode 100644
index 00000000000..907e640161a
--- /dev/null
+++ b/app/assets/javascripts/u2f/util.js
@@ -0,0 +1,13 @@
+(function() {
+ this.U2FUtil = (function() {
+ function U2FUtil() {}
+
+ U2FUtil.isU2FSupported = function() {
+ return window.u2f;
+ };
+
+ return U2FUtil;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/u2f/util.js.coffee.erb b/app/assets/javascripts/u2f/util.js.coffee.erb
deleted file mode 100644
index d59341c38b9..00000000000
--- a/app/assets/javascripts/u2f/util.js.coffee.erb
+++ /dev/null
@@ -1,15 +0,0 @@
-# Helper class for U2F (universal 2nd factor) device registration and authentication.
-
-class @U2FUtil
- @isU2FSupported: ->
- if @testMode
- true
- else
- gon.u2f.browser_supports_u2f
-
- @enableTestMode: ->
- @testMode = true
-
-<% if Rails.env.test? %>
-U2FUtil.enableTestMode();
-<% end %>
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
new file mode 100644
index 00000000000..6c4d88cf407
--- /dev/null
+++ b/app/assets/javascripts/user.js
@@ -0,0 +1,29 @@
+(function() {
+ this.User = (function() {
+ function User(opts) {
+ this.opts = opts;
+ $('.profile-groups-avatars').tooltip({
+ "placement": "top"
+ });
+ this.initTabs();
+ $('.hide-project-limit-message').on('click', function(e) {
+ $.cookie('hide_project_limit_message', 'false', {
+ path: gon.relative_url_root || '/'
+ });
+ $(this).parents('.project-limit-message').remove();
+ return e.preventDefault();
+ });
+ }
+
+ User.prototype.initTabs = function() {
+ return new UserTabs({
+ parentEl: '.user-profile',
+ action: this.opts.action
+ });
+ };
+
+ return User;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
deleted file mode 100644
index 2882a90d118..00000000000
--- a/app/assets/javascripts/user.js.coffee
+++ /dev/null
@@ -1,17 +0,0 @@
-class @User
- constructor: (@opts) ->
- $('.profile-groups-avatars').tooltip("placement": "top")
-
- @initTabs()
-
- $('.hide-project-limit-message').on 'click', (e) ->
- path = '/'
- $.cookie('hide_project_limit_message', 'false', { path: path })
- $(@).parents('.project-limit-message').remove()
- e.preventDefault()
-
- initTabs: ->
- new UserTabs(
- parentEl: '.user-profile'
- action: @opts.action
- )
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
new file mode 100644
index 00000000000..e5e75701fee
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js
@@ -0,0 +1,119 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.UserTabs = (function() {
+ function UserTabs(opts) {
+ this.tabShown = bind(this.tabShown, this);
+ var i, item, len, ref, ref1, ref2, ref3;
+ this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
+ if (typeof this.parentEl === 'string') {
+ this.parentEl = $(this.parentEl);
+ }
+ this._location = location;
+ this.loaded = {};
+ ref3 = this.parentEl.find('.nav-links a');
+ for (i = 0, len = ref3.length; i < len; i++) {
+ item = ref3[i];
+ this.loaded[$(item).attr('data-action')] = false;
+ }
+ this.actions = Object.keys(this.loaded);
+ this.bindEvents();
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+ this.activateTab(this.action);
+ }
+
+ UserTabs.prototype.bindEvents = function() {
+ return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
+ };
+
+ UserTabs.prototype.tabShown = function(event) {
+ var $target, action, source;
+ $target = $(event.target);
+ action = $target.data('action');
+ source = $target.attr('href');
+ this.setTab(source, action);
+ return this.setCurrentAction(action);
+ };
+
+ UserTabs.prototype.activateTab = function(action) {
+ return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
+ };
+
+ UserTabs.prototype.setTab = function(source, action) {
+ if (this.loaded[action] === true) {
+ return;
+ }
+ if (action === 'activity') {
+ this.loadActivities(source);
+ }
+ if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
+ return this.loadTab(source, action);
+ }
+ };
+
+ UserTabs.prototype.loadTab = function(source, action) {
+ return $.ajax({
+ beforeSend: (function(_this) {
+ return function() {
+ return _this.toggleLoading(true);
+ };
+ })(this),
+ complete: (function(_this) {
+ return function() {
+ return _this.toggleLoading(false);
+ };
+ })(this),
+ dataType: 'json',
+ type: 'GET',
+ url: source + ".json",
+ success: (function(_this) {
+ return function(data) {
+ var tabSelector;
+ tabSelector = 'div#' + action;
+ _this.parentEl.find(tabSelector).html(data.html);
+ _this.loaded[action] = true;
+ return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ };
+ })(this)
+ });
+ };
+
+ UserTabs.prototype.loadActivities = function(source) {
+ var $calendarWrap;
+ if (this.loaded['activity'] === true) {
+ return;
+ }
+ $calendarWrap = this.parentEl.find('.user-calendar');
+ $calendarWrap.load($calendarWrap.data('href'));
+ new Activities();
+ return this.loaded['activity'] = true;
+ };
+
+ UserTabs.prototype.toggleLoading = function(status) {
+ return this.parentEl.find('.loading-status .loading').toggle(status);
+ };
+
+ UserTabs.prototype.setCurrentAction = function(action) {
+ var new_state, regExp;
+ regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
+ new_state = this._location.pathname;
+ new_state = new_state.replace(/\/+$/, "");
+ new_state = new_state.replace(regExp, '');
+ if (action !== this.defaultAction) {
+ new_state += "/" + action;
+ }
+ new_state += this._location.search + this._location.hash;
+ history.replaceState({
+ turbolinks: true,
+ url: new_state
+ }, document.title, new_state);
+ return new_state;
+ };
+
+ return UserTabs;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/user_tabs.js.coffee b/app/assets/javascripts/user_tabs.js.coffee
deleted file mode 100644
index 29dad21faed..00000000000
--- a/app/assets/javascripts/user_tabs.js.coffee
+++ /dev/null
@@ -1,156 +0,0 @@
-# UserTabs
-#
-# Handles persisting and restoring the current tab selection and lazily-loading
-# content on the Users#show page.
-#
-# ### Example Markup
-#
-# <ul class="nav-links">
-# <li class="activity-tab active">
-# <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
-# Activity
-# </a>
-# </li>
-# <li class="groups-tab">
-# <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
-# Groups
-# </a>
-# </li>
-# <li class="contributed-tab">
-# <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
-# Contributed projects
-# </a>
-# </li>
-# <li class="projects-tab">
-# <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
-# Personal projects
-# </a>
-# </li>
-# <li class="snippets-tab">
-# <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
-# </a>
-# </li>
-# </ul>
-#
-# <div class="tab-content">
-# <div class="tab-pane" id="activity">
-# Activity Content
-# </div>
-# <div class="tab-pane" id="groups">
-# Groups Content
-# </div>
-# <div class="tab-pane" id="contributed">
-# Contributed projects content
-# </div>
-# <div class="tab-pane" id="projects">
-# Projects content
-# </div>
-# <div class="tab-pane" id="snippets">
-# Snippets content
-# </div>
-# </div>
-#
-# <div class="loading-status">
-# <div class="loading">
-# Loading Animation
-# </div>
-# </div>
-#
-class @UserTabs
- constructor: (opts) ->
- {
- @action = 'activity'
- @defaultAction = 'activity'
- @parentEl = $(document)
- } = opts
-
- # Make jQuery object if selector is provided
- @parentEl = $(@parentEl) if typeof @parentEl is 'string'
-
- # Store the `location` object, allowing for easier stubbing in tests
- @_location = location
-
- # Set tab states
- @loaded = {}
- for item in @parentEl.find('.nav-links a')
- @loaded[$(item).attr 'data-action'] = false
-
- # Actions
- @actions = Object.keys @loaded
-
- @bindEvents()
-
- # Set active tab
- @action = @defaultAction if @action is 'show'
- @activateTab(@action)
-
- bindEvents: ->
- # Toggle event listeners
- @parentEl
- .off 'shown.bs.tab', '.nav-links a[data-toggle="tab"]'
- .on 'shown.bs.tab', '.nav-links a[data-toggle="tab"]', @tabShown
-
- tabShown: (event) =>
- $target = $(event.target)
- action = $target.data('action')
- source = $target.attr('href')
-
- @setTab(source, action)
- @setCurrentAction(action)
-
- activateTab: (action) ->
- @parentEl.find(".nav-links .js-#{action}-tab a").tab('show')
-
- setTab: (source, action) ->
- return if @loaded[action] is true
-
- if action is 'activity'
- @loadActivities(source)
-
- if action in ['groups', 'contributed', 'projects', 'snippets']
- @loadTab(source, action)
-
- loadTab: (source, action) ->
- $.ajax
- beforeSend: => @toggleLoading(true)
- complete: => @toggleLoading(false)
- dataType: 'json'
- type: 'GET'
- url: "#{source}.json"
- success: (data) =>
- tabSelector = 'div#' + action
- @parentEl.find(tabSelector).html(data.html)
- @loaded[action] = true
-
- # Fix tooltips
- gl.utils.localTimeAgo($('.js-timeago', tabSelector))
-
- loadActivities: (source) ->
- return if @loaded['activity'] is true
-
- $calendarWrap = @parentEl.find('.user-calendar')
- $calendarWrap.load($calendarWrap.data('href'))
-
- new Activities()
- @loaded['activity'] = true
-
- toggleLoading: (status) ->
- @parentEl.find('.loading-status .loading').toggle(status)
-
- setCurrentAction: (action) ->
- # Remove possible actions from URL
- regExp = new RegExp('\/(' + @actions.join('|') + ')(\.html)?\/?$')
- new_state = @_location.pathname
- new_state = new_state.replace(/\/+$/, "") # remove trailing slashes
- new_state = new_state.replace(regExp, '')
-
- # Append the new action if we're on a tab other than 'activity'
- unless action == @defaultAction
- new_state += "/#{action}"
-
- # Ensure parameters and hash come along for the ride
- new_state += @_location.search + @_location.hash
-
- history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
-
- new_state
diff --git a/app/assets/javascripts/users/application.js.coffee b/app/assets/javascripts/users/application.js.coffee
deleted file mode 100644
index 91cacfece46..00000000000
--- a/app/assets/javascripts/users/application.js.coffee
+++ /dev/null
@@ -1,2 +0,0 @@
-#
-#= require_tree .
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
new file mode 100644
index 00000000000..74ecf4f4cf9
--- /dev/null
+++ b/app/assets/javascripts/users/calendar.js
@@ -0,0 +1,201 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.Calendar = (function() {
+ function Calendar(timestamps, calendar_activities_path) {
+ this.calendar_activities_path = calendar_activities_path;
+ this.clickDay = bind(this.clickDay, this);
+ this.currentSelectedDate = '';
+ this.daySpace = 1;
+ this.daySize = 15;
+ this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
+ this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.months = [];
+ this.timestampsTmp = [];
+ var group = 0;
+
+ var today = new Date()
+ today.setHours(0, 0, 0, 0, 0);
+
+ var oneYearAgo = new Date(today);
+ oneYearAgo.setFullYear(today.getFullYear() - 1);
+
+ var days = gl.utils.getDayDifference(oneYearAgo, today);
+
+ for(var i = 0; i <= days; i++) {
+ var date = new Date(oneYearAgo);
+ date.setDate(date.getDate() + i);
+
+ var day = date.getDay();
+ var count = timestamps[date.getTime() * 0.001];
+
+ if ((day === 0 && i !== 0) || i === 0) {
+ this.timestampsTmp.push([]);
+ group++;
+ }
+
+ var innerArray = this.timestampsTmp[group - 1];
+ innerArray.push({
+ count: count || 0,
+ date: date,
+ day: day
+ });
+ }
+
+ this.colorKey = this.initColorKey();
+ this.color = this.initColor();
+ this.renderSvg(group);
+ this.renderDays();
+ this.renderMonths();
+ this.renderDayTitles();
+ this.renderKey();
+ this.initTooltips();
+ }
+
+ Calendar.prototype.renderSvg = function(group) {
+ return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar');
+ };
+
+ Calendar.prototype.renderDays = function() {
+ return this.svg.selectAll('g').data(this.timestampsTmp).enter().append('g').attr('transform', (function(_this) {
+ return function(group, i) {
+ _.each(group, function(stamp, a) {
+ var lastMonth, lastMonthX, month, x;
+ if (a === 0 && stamp.day === 0) {
+ month = stamp.date.getMonth();
+ x = (_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace;
+ lastMonth = _.last(_this.months);
+ if (lastMonth != null) {
+ lastMonthX = lastMonth.x;
+ }
+ if (lastMonth == null) {
+ return _this.months.push({
+ month: month,
+ x: x
+ });
+ } else if (month !== lastMonth.month && x - _this.daySizeWithSpace !== lastMonthX) {
+ return _this.months.push({
+ month: month,
+ x: x
+ });
+ }
+ }
+ });
+ return "translate(" + ((_this.daySizeWithSpace * i + 1) + _this.daySizeWithSpace) + ", 18)";
+ };
+ })(this)).selectAll('rect').data(function(stamp) {
+ return stamp;
+ }).enter().append('rect').attr('x', '0').attr('y', (function(_this) {
+ return function(stamp, i) {
+ return _this.daySizeWithSpace * stamp.day;
+ };
+ })(this)).attr('width', this.daySize).attr('height', this.daySize).attr('title', (function(_this) {
+ return function(stamp) {
+ var contribText, date, dateText;
+ date = new Date(stamp.date);
+ contribText = 'No contributions';
+ if (stamp.count > 0) {
+ contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : '');
+ }
+ dateText = dateFormat(date, 'mmm d, yyyy');
+ return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText;
+ };
+ })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) {
+ return function(stamp) {
+ if (stamp.count !== 0) {
+ return _this.color(Math.min(stamp.count, 40));
+ } else {
+ return '#ededed';
+ }
+ };
+ })(this)).attr('data-container', 'body').on('click', this.clickDay);
+ };
+
+ Calendar.prototype.renderDayTitles = function() {
+ var days;
+ days = [
+ {
+ text: 'M',
+ y: 29 + (this.daySizeWithSpace * 1)
+ }, {
+ text: 'W',
+ y: 29 + (this.daySizeWithSpace * 3)
+ }, {
+ text: 'F',
+ y: 29 + (this.daySizeWithSpace * 5)
+ }
+ ];
+ return this.svg.append('g').selectAll('text').data(days).enter().append('text').attr('text-anchor', 'middle').attr('x', 8).attr('y', function(day) {
+ return day.y;
+ }).text(function(day) {
+ return day.text;
+ }).attr('class', 'user-contrib-text');
+ };
+
+ Calendar.prototype.renderMonths = function() {
+ return this.svg.append('g').selectAll('text').data(this.months).enter().append('text').attr('x', function(date) {
+ return date.x;
+ }).attr('y', 10).attr('class', 'user-contrib-text').text((function(_this) {
+ return function(date) {
+ return _this.monthNames[date.month];
+ };
+ })(this));
+ };
+
+ Calendar.prototype.renderKey = function() {
+ var keyColors;
+ keyColors = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ return this.svg.append('g').attr('transform', "translate(18, " + (this.daySizeWithSpace * 8 + 16) + ")").selectAll('rect').data(keyColors).enter().append('rect').attr('width', this.daySize).attr('height', this.daySize).attr('x', (function(_this) {
+ return function(color, i) {
+ return _this.daySizeWithSpace * i;
+ };
+ })(this)).attr('y', 0).attr('fill', function(color) {
+ return color;
+ });
+ };
+
+ Calendar.prototype.initColor = function() {
+ var colorRange;
+ colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
+ return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
+ };
+
+ Calendar.prototype.initColorKey = function() {
+ return d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+ };
+
+ Calendar.prototype.clickDay = function(stamp) {
+ var formatted_date;
+ if (this.currentSelectedDate !== stamp.date) {
+ this.currentSelectedDate = stamp.date;
+ formatted_date = this.currentSelectedDate.getFullYear() + "-" + (this.currentSelectedDate.getMonth() + 1) + "-" + this.currentSelectedDate.getDate();
+ return $.ajax({
+ url: this.calendar_activities_path,
+ data: {
+ date: formatted_date
+ },
+ cache: false,
+ dataType: 'html',
+ beforeSend: function() {
+ return $('.user-calendar-activities').html('<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>');
+ },
+ success: function(data) {
+ return $('.user-calendar-activities').html(data);
+ }
+ });
+ } else {
+ return $('.user-calendar-activities').html('');
+ }
+ };
+
+ Calendar.prototype.initTooltips = function() {
+ return $('.js-contrib-calendar .js-tooltip').tooltip({
+ html: true
+ });
+ };
+
+ return Calendar;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/users/calendar.js.coffee b/app/assets/javascripts/users/calendar.js.coffee
deleted file mode 100644
index c081f023b04..00000000000
--- a/app/assets/javascripts/users/calendar.js.coffee
+++ /dev/null
@@ -1,193 +0,0 @@
-class @Calendar
- constructor: (timestamps, @calendar_activities_path) ->
- @currentSelectedDate = ''
- @daySpace = 1
- @daySize = 15
- @daySizeWithSpace = @daySize + (@daySpace * 2)
- @monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
- @months = []
-
- # Loop through the timestamps to create a group of objects
- # The group of objects will be grouped based on the day of the week they are
- @timestampsTmp = []
- i = 0
- group = 0
- _.each timestamps, (count, date) =>
- newDate = new Date parseInt(date) * 1000
- day = newDate.getDay()
-
- # Create a new group array if this is the first day of the week
- # or if is first object
- if (day is 0 and i isnt 0) or i is 0
- @timestampsTmp.push []
- group++
-
- innerArray = @timestampsTmp[group-1]
-
- # Push to the inner array the values that will be used to render map
- innerArray.push
- count: count
- date: newDate
- day: day
-
- i++
-
- # Init color functions
- @colorKey = @initColorKey()
- @color = @initColor()
-
- # Init the svg element
- @renderSvg(group)
- @renderDays()
- @renderMonths()
- @renderDayTitles()
- @renderKey()
-
- @initTooltips()
-
- renderSvg: (group) ->
- @svg = d3.select '.js-contrib-calendar'
- .append 'svg'
- .attr 'width', (group + 1) * @daySizeWithSpace
- .attr 'height', 167
- .attr 'class', 'contrib-calendar'
-
- renderDays: ->
- @svg.selectAll 'g'
- .data @timestampsTmp
- .enter()
- .append 'g'
- .attr 'transform', (group, i) =>
- _.each group, (stamp, a) =>
- if a is 0 and stamp.day is 0
- month = stamp.date.getMonth()
- x = (@daySizeWithSpace * i + 1) + @daySizeWithSpace
- lastMonth = _.last(@months)
- if lastMonth?
- lastMonthX = lastMonth.x
-
- if !lastMonth?
- @months.push
- month: month
- x: x
- else if month isnt lastMonth.month and x - @daySizeWithSpace isnt lastMonthX
- @months.push
- month: month
- x: x
-
- "translate(#{(@daySizeWithSpace * i + 1) + @daySizeWithSpace}, 18)"
- .selectAll 'rect'
- .data (stamp) ->
- stamp
- .enter()
- .append 'rect'
- .attr 'x', '0'
- .attr 'y', (stamp, i) =>
- (@daySizeWithSpace * stamp.day)
- .attr 'width', @daySize
- .attr 'height', @daySize
- .attr 'title', (stamp) =>
- contribText = 'No contributions'
-
- if stamp.count > 0
- contribText = "#{stamp.count} contribution#{if stamp.count > 1 then 's' else ''}"
-
- date = dateFormat(stamp.date, 'mmm d, yyyy')
-
- "#{contribText}<br />#{date}"
- .attr 'class', 'user-contrib-cell js-tooltip'
- .attr 'fill', (stamp) =>
- if stamp.count isnt 0
- @color(Math.min(stamp.count, 40))
- else
- '#ededed'
- .attr 'data-container', 'body'
- .on 'click', @clickDay
-
- renderDayTitles: ->
- days = [{
- text: 'M'
- y: 29 + (@daySizeWithSpace * 1)
- }, {
- text: 'W'
- y: 29 + (@daySizeWithSpace * 3)
- }, {
- text: 'F'
- y: 29 + (@daySizeWithSpace * 5)
- }]
- @svg.append 'g'
- .selectAll 'text'
- .data days
- .enter()
- .append 'text'
- .attr 'text-anchor', 'middle'
- .attr 'x', 8
- .attr 'y', (day) ->
- day.y
- .text (day) ->
- day.text
- .attr 'class', 'user-contrib-text'
-
- renderMonths: ->
- @svg.append 'g'
- .selectAll 'text'
- .data @months
- .enter()
- .append 'text'
- .attr 'x', (date) ->
- date.x
- .attr 'y', 10
- .attr 'class', 'user-contrib-text'
- .text (date) =>
- @monthNames[date.month]
-
- renderKey: ->
- keyColors = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
- @svg.append 'g'
- .attr 'transform', "translate(18, #{@daySizeWithSpace * 8 + 16})"
- .selectAll 'rect'
- .data keyColors
- .enter()
- .append 'rect'
- .attr 'width', @daySize
- .attr 'height', @daySize
- .attr 'x', (color, i) =>
- @daySizeWithSpace * i
- .attr 'y', 0
- .attr 'fill', (color) ->
- color
-
- initColor: ->
- colorRange = ['#ededed', @colorKey(0), @colorKey(1), @colorKey(2), @colorKey(3)]
- d3.scale
- .threshold()
- .domain([0, 10, 20, 30])
- .range(colorRange)
-
- initColorKey: ->
- d3.scale
- .linear()
- .range(['#acd5f2', '#254e77'])
- .domain([0, 3])
-
- clickDay: (stamp) =>
- if @currentSelectedDate isnt stamp.date
- @currentSelectedDate = stamp.date
- formatted_date = @currentSelectedDate.getFullYear() + "-" + (@currentSelectedDate.getMonth()+1) + "-" + @currentSelectedDate.getDate()
-
- $.ajax
- url: @calendar_activities_path
- data:
- date: formatted_date
- cache: false
- dataType: 'html'
- beforeSend: ->
- $('.user-calendar-activities').html '<div class="text-center"><i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i></div>'
- success: (data) ->
- $('.user-calendar-activities').html data
- else
- $('.user-calendar-activities').html ''
-
- initTooltips: ->
- $('.js-contrib-calendar .js-tooltip').tooltip
- html: true
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
new file mode 100644
index 00000000000..b95faadc8e7
--- /dev/null
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -0,0 +1,7 @@
+
+/*= require_tree . */
+
+(function() {
+
+
+}).call(this);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
new file mode 100644
index 00000000000..bad82868ab0
--- /dev/null
+++ b/app/assets/javascripts/users_select.js
@@ -0,0 +1,355 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
+ slice = [].slice;
+
+ this.UsersSelect = (function() {
+ function UsersSelect(currentUser) {
+ this.users = bind(this.users, this);
+ this.user = bind(this.user, this);
+ this.usersPath = "/autocomplete/users.json";
+ this.userPath = "/autocomplete/users/:id.json";
+ if (currentUser != null) {
+ this.currentUser = JSON.parse(currentUser);
+ }
+ $('.js-user-search').each((function(_this) {
+ return function(i, dropdown) {
+ var options = {};
+ var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser;
+ $dropdown = $(dropdown);
+ options.projectId = $dropdown.data('project-id');
+ options.showCurrentUser = $dropdown.data('current-user');
+ showNullUser = $dropdown.data('null-user');
+ showAnyUser = $dropdown.data('any-user');
+ firstUser = $dropdown.data('first-user');
+ options.authorId = $dropdown.data('author-id');
+ selectedId = $dropdown.data('selected');
+ defaultLabel = $dropdown.data('default-label');
+ issueURL = $dropdown.data('issueUpdate');
+ $selectbox = $dropdown.closest('.selectbox');
+ $block = $selectbox.closest('.block');
+ abilityName = $dropdown.data('ability-name');
+ $value = $block.find('.value');
+ $collapsedSidebar = $block.find('.sidebar-collapsed-user');
+ $loading = $block.find('.block-loading').fadeOut();
+ $block.on('click', '.js-assign-yourself', function(e) {
+ e.preventDefault();
+ return assignTo(_this.currentUser.id);
+ });
+ assignTo = function(selected) {
+ var data;
+ data = {};
+ data[abilityName] = {};
+ data[abilityName].assignee_id = selected != null ? selected : null;
+ $loading.fadeIn();
+ $dropdown.trigger('loading.gl.dropdown');
+ return $.ajax({
+ type: 'PUT',
+ dataType: 'json',
+ url: issueURL,
+ data: data
+ }).done(function(data) {
+ var user;
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ $selectbox.hide();
+ if (data.assignee) {
+ user = {
+ name: data.assignee.name,
+ username: data.assignee.username,
+ avatar: data.assignee.avatar_url
+ };
+ } else {
+ user = {
+ name: 'Unassigned',
+ username: '',
+ avatar: ''
+ };
+ }
+ $value.html(assigneeTemplate(user));
+ $collapsedSidebar.attr('title', user.name).tooltip('fixTitle');
+ return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
+ });
+ };
+ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+ assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+ return $dropdown.glDropdown({
+ data: function(term, callback) {
+ var isAuthorFilter;
+ isAuthorFilter = $('.js-author-search');
+ return _this.users(term, options, function(users) {
+ var anyUser, index, j, len, name, obj, showDivider;
+ if (term.length === 0) {
+ showDivider = 0;
+ if (firstUser) {
+ for (index = j = 0, len = users.length; j < len; index = ++j) {
+ obj = users[index];
+ if (obj.username === firstUser) {
+ users.splice(index, 1);
+ users.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ showDivider += 1;
+ users.unshift({
+ beforeDivider: true,
+ name: 'Unassigned',
+ id: 0
+ });
+ }
+ if (showAnyUser) {
+ showDivider += 1;
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ beforeDivider: true,
+ name: name,
+ id: null
+ };
+ users.unshift(anyUser);
+ }
+ }
+ if (showDivider) {
+ users.splice(showDivider, 0, "divider");
+ }
+ return callback(users);
+ });
+ },
+ filterable: true,
+ filterRemote: true,
+ search: {
+ fields: ['name', 'username']
+ },
+ selectable: true,
+ fieldName: $dropdown.data('field-name'),
+ toggleLabel: function(selected) {
+ if (selected && 'id' in selected) {
+ if (selected.text) {
+ return selected.text;
+ } else {
+ return selected.name;
+ }
+ } else {
+ return defaultLabel;
+ }
+ },
+ inputId: 'issue_assignee_id',
+ hidden: function(e) {
+ $selectbox.hide();
+ return $value.css('display', '');
+ },
+ clicked: function(user, $el, e) {
+ var isIssueIndex, isMRIndex, page, selected;
+ page = $('body').data('page');
+ isIssueIndex = page === 'projects:issues:index';
+ isMRIndex = (page === page && page === 'projects:merge_requests:index');
+ if ($dropdown.hasClass('js-filter-bulk-update')) {
+ return;
+ }
+ if (page === 'projects:boards:show') {
+ selectedId = user.id;
+ gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
+ gl.issueBoards.BoardsStore.updateFiltersUrl();
+ e.preventDefault();
+ } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ selectedId = user.id;
+ return Issuable.filterResults($dropdown.closest('form'));
+ } else if ($dropdown.hasClass('js-filter-submit')) {
+ return $dropdown.closest('form').submit();
+ } else {
+ selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
+ return assignTo(selected);
+ }
+ },
+ renderRow: function(user) {
+ var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
+ username = user.username ? "@" + user.username : "";
+ avatar = user.avatar_url ? user.avatar_url : false;
+ selected = user.id === selectedId ? "is-active" : "";
+ img = "";
+ if (user.beforeDivider != null) {
+ "<li> <a href='#' class='" + selected + "'> " + user.name + " </a> </li>";
+ } else {
+ if (avatar) {
+ img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
+ }
+ }
+ listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
+ listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
+ listClosingTags = "</a> </li>";
+ if (username === '') {
+ listWithUserName = '';
+ }
+ return listWithName + listWithUserName + listClosingTags;
+ }
+ });
+ };
+ })(this));
+ $('.ajax-users-select').each((function(_this) {
+ return function(i, select) {
+ var firstUser, showAnyUser, showEmailUser, showNullUser;
+ var options = {};
+ options.skipLdap = $(select).hasClass('skip_ldap');
+ options.projectId = $(select).data('project-id');
+ options.groupId = $(select).data('group-id');
+ options.showCurrentUser = $(select).data('current-user');
+ options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches');
+ options.authorId = $(select).data('author-id');
+ options.skipUsers = $(select).data('skip-users');
+ showNullUser = $(select).data('null-user');
+ showAnyUser = $(select).data('any-user');
+ showEmailUser = $(select).data('email-user');
+ firstUser = $(select).data('first-user');
+ return $(select).select2({
+ placeholder: "Search for a user",
+ multiple: $(select).hasClass('multiselect'),
+ minimumInputLength: 0,
+ query: function(query) {
+ return _this.users(query.term, options, function(users) {
+ var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref;
+ data = {
+ results: users
+ };
+ if (query.term.length === 0) {
+ if (firstUser) {
+ ref = data.results;
+ for (index = j = 0, len = ref.length; j < len; index = ++j) {
+ obj = ref[index];
+ if (obj.username === firstUser) {
+ data.results.splice(index, 1);
+ data.results.unshift(obj);
+ break;
+ }
+ }
+ }
+ if (showNullUser) {
+ nullUser = {
+ name: 'Unassigned',
+ id: 0
+ };
+ data.results.unshift(nullUser);
+ }
+ if (showAnyUser) {
+ name = showAnyUser;
+ if (name === true) {
+ name = 'Any User';
+ }
+ anyUser = {
+ name: name,
+ id: null
+ };
+ data.results.unshift(anyUser);
+ }
+ }
+ if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) {
+ emailUser = {
+ name: "Invite \"" + query.term + "\"",
+ username: query.term,
+ id: query.term
+ };
+ data.results.unshift(emailUser);
+ }
+ return query.callback(data);
+ });
+ },
+ initSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.initSelection.apply(_this, args);
+ },
+ formatResult: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatResult.apply(_this, args);
+ },
+ formatSelection: function() {
+ var args;
+ args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
+ return _this.formatSelection.apply(_this, args);
+ },
+ dropdownCssClass: "ajax-users-dropdown",
+ escapeMarkup: function(m) {
+ return m;
+ }
+ });
+ };
+ })(this));
+ }
+
+ UsersSelect.prototype.initSelection = function(element, callback) {
+ var id, nullUser;
+ id = $(element).val();
+ if (id === "0") {
+ nullUser = {
+ name: 'Unassigned'
+ };
+ return callback(nullUser);
+ } else if (id !== "") {
+ return this.user(id, callback);
+ }
+ };
+
+ UsersSelect.prototype.formatResult = function(user) {
+ var avatar;
+ if (user.avatar_url) {
+ avatar = user.avatar_url;
+ } else {
+ avatar = gon.default_avatar_url;
+ }
+ return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>";
+ };
+
+ UsersSelect.prototype.formatSelection = function(user) {
+ return user.name;
+ };
+
+ UsersSelect.prototype.user = function(user_id, callback) {
+ var url;
+ url = this.buildUrl(this.userPath);
+ url = url.replace(':id', user_id);
+ return $.ajax({
+ url: url,
+ dataType: "json"
+ }).done(function(user) {
+ return callback(user);
+ });
+ };
+
+ UsersSelect.prototype.users = function(query, options, callback) {
+ var url;
+ url = this.buildUrl(this.usersPath);
+ return $.ajax({
+ url: url,
+ data: {
+ search: query,
+ per_page: 20,
+ active: true,
+ project_id: options.projectId || null,
+ group_id: options.groupId || null,
+ skip_ldap: options.skipLdap || null,
+ current_user: options.showCurrentUser || null,
+ push_code_to_protected_branches: options.pushCodeToProtectedBranches || null,
+ author_id: options.authorId || null,
+ skip_users: options.skipUsers || null
+ },
+ dataType: "json"
+ }).done(function(users) {
+ return callback(users);
+ });
+ };
+
+ UsersSelect.prototype.buildUrl = function(url) {
+ if (gon.relative_url_root != null) {
+ url = gon.relative_url_root.replace(/\/$/, '') + url;
+ }
+ return url;
+ };
+
+ return UsersSelect;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
deleted file mode 100644
index 4e032ab1ff1..00000000000
--- a/app/assets/javascripts/users_select.js.coffee
+++ /dev/null
@@ -1,326 +0,0 @@
-class @UsersSelect
- constructor: (currentUser) ->
- @usersPath = "/autocomplete/users.json"
- @userPath = "/autocomplete/users/:id.json"
- if currentUser?
- @currentUser = JSON.parse(currentUser)
-
- $('.js-user-search').each (i, dropdown) =>
- $dropdown = $(dropdown)
- @projectId = $dropdown.data('project-id')
- @showCurrentUser = $dropdown.data('current-user')
- showNullUser = $dropdown.data('null-user')
- showAnyUser = $dropdown.data('any-user')
- firstUser = $dropdown.data('first-user')
- @authorId = $dropdown.data('author-id')
- selectedId = $dropdown.data('selected')
- defaultLabel = $dropdown.data('default-label')
- issueURL = $dropdown.data('issueUpdate')
- $selectbox = $dropdown.closest('.selectbox')
- $block = $selectbox.closest('.block')
- abilityName = $dropdown.data('ability-name')
- $value = $block.find('.value')
- $collapsedSidebar = $block.find('.sidebar-collapsed-user')
- $loading = $block.find('.block-loading').fadeOut()
-
- $block.on('click', '.js-assign-yourself', (e) =>
- e.preventDefault()
- assignTo(@currentUser.id)
- )
-
- assignTo = (selected) ->
- data = {}
- data[abilityName] = {}
- data[abilityName].assignee_id = if selected? then selected else null
- $loading
- .fadeIn()
- $dropdown.trigger('loading.gl.dropdown')
- $.ajax(
- type: 'PUT'
- dataType: 'json'
- url: issueURL
- data: data
- ).done (data) ->
- $dropdown.trigger('loaded.gl.dropdown')
- $loading.fadeOut()
- $selectbox.hide()
-
- if data.assignee
- user =
- name: data.assignee.name
- username: data.assignee.username
- avatar: data.assignee.avatar_url
- else
- user =
- name: 'Unassigned'
- username: ''
- avatar: ''
- $value.html(assigneeTemplate(user))
- $collapsedSidebar.html(collapsedAssigneeTemplate(user))
-
-
- collapsedAssigneeTemplate = _.template(
- '<% if( avatar ) { %>
- <a class="author_link" href="/u/<%- username %>">
- <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>">
- <span class="author">Toni Boehm</span>
- </a>
- <% } else { %>
- <i class="fa fa-user"></i>
- <% } %>'
- )
-
- assigneeTemplate = _.template(
- '<% if (username) { %>
- <a class="author_link bold" href="/u/<%- username %>">
- <% if( avatar ) { %>
- <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>">
- <% } %>
- <span class="author"><%- name %></span>
- <span class="username">
- @<%- username %>
- </span>
- </a>
- <% } else { %>
- <span class="no-value assign-yourself">
- No assignee -
- <a href="#" class="js-assign-yourself">
- assign yourself
- </a>
- </span>
- <% } %>'
- )
-
- $dropdown.glDropdown(
- data: (term, callback) =>
- isAuthorFilter = $('.js-author-search')
-
- @users term, (users) =>
- if term.length is 0
- showDivider = 0
-
- if firstUser
- # Move current user to the front of the list
- for obj, index in users
- if obj.username == firstUser
- users.splice(index, 1)
- users.unshift(obj)
- break
-
- if showNullUser
- showDivider += 1
- users.unshift(
- beforeDivider: true
- name: 'Unassigned',
- id: 0
- )
-
- if showAnyUser
- showDivider += 1
- name = showAnyUser
- name = 'Any User' if name == true
- anyUser = {
- beforeDivider: true
- name: name,
- id: null
- }
- users.unshift(anyUser)
-
- if showDivider
- users.splice(showDivider, 0, "divider")
-
- # Send the data back
- callback users
- filterable: true
- filterRemote: true
- search:
- fields: ['name', 'username']
- selectable: true
- fieldName: $dropdown.data('field-name')
-
- toggleLabel: (selected) ->
- if selected && 'id' of selected
- if selected.text then selected.text else selected.name
- else
- defaultLabel
-
- inputId: 'issue_assignee_id'
-
- hidden: (e) ->
- $selectbox.hide()
- # display:block overrides the hide-collapse rule
- $value.css('display', '')
-
- clicked: (user) ->
- page = $('body').data 'page'
- isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is page is 'projects:merge_requests:index'
- if $dropdown.hasClass('js-filter-bulk-update')
- return
-
- if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- selectedId = user.id
- Issuable.filterResults $dropdown.closest('form')
- else if $dropdown.hasClass 'js-filter-submit'
- $dropdown.closest('form').submit()
- else
- selected = $dropdown
- .closest('.selectbox')
- .find("input[name='#{$dropdown.data('field-name')}']").val()
- assignTo(selected)
-
- renderRow: (user) ->
- username = if user.username then "@#{user.username}" else ""
- avatar = if user.avatar_url then user.avatar_url else false
- selected = if user.id is selectedId then "is-active" else ""
- img = ""
-
- if user.beforeDivider?
- "<li>
- <a href='#' class='#{selected}'>
- #{user.name}
- </a>
- </li>"
- else
- if avatar
- img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
-
- # split into three parts so we can remove the username section if nessesary
- listWithName = "<li>
- <a href='#' class='dropdown-menu-user-link #{selected}'>
- #{img}
- <strong class='dropdown-menu-user-full-name'>
- #{user.name}
- </strong>"
-
- listWithUserName = "<span class='dropdown-menu-user-username'>
- #{username}
- </span>"
- listClosingTags = "</a>
- </li>"
-
-
- if username is ''
- listWithUserName = ''
-
- listWithName + listWithUserName + listClosingTags
- )
-
- $('.ajax-users-select').each (i, select) =>
- @projectId = $(select).data('project-id')
- @groupId = $(select).data('group-id')
- @showCurrentUser = $(select).data('current-user')
- @authorId = $(select).data('author-id')
- showNullUser = $(select).data('null-user')
- showAnyUser = $(select).data('any-user')
- showEmailUser = $(select).data('email-user')
- firstUser = $(select).data('first-user')
-
- $(select).select2
- placeholder: "Search for a user"
- multiple: $(select).hasClass('multiselect')
- minimumInputLength: 0
- query: (query) =>
- @users query.term, (users) =>
- data = { results: users }
-
- if query.term.length == 0
- if firstUser
- # Move current user to the front of the list
- for obj, index in data.results
- if obj.username == firstUser
- data.results.splice(index, 1)
- data.results.unshift(obj)
- break
-
- if showNullUser
- nullUser = {
- name: 'Unassigned',
- id: 0
- }
- data.results.unshift(nullUser)
-
- if showAnyUser
- name = showAnyUser
- name = 'Any User' if name == true
- anyUser = {
- name: name,
- id: null
- }
- data.results.unshift(anyUser)
-
- if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
- emailUser = {
- name: "Invite \"#{query.term}\"",
- username: query.term,
- id: query.term
- }
- data.results.unshift(emailUser)
-
- query.callback(data)
-
- initSelection: (args...) =>
- @initSelection(args...)
- formatResult: (args...) =>
- @formatResult(args...)
- formatSelection: (args...) =>
- @formatSelection(args...)
- dropdownCssClass: "ajax-users-dropdown"
- escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
- m
-
- initSelection: (element, callback) ->
- id = $(element).val()
- if id == "0"
- nullUser = { name: 'Unassigned' }
- callback(nullUser)
- else if id != ""
- @user(id, callback)
-
- formatResult: (user) ->
- if user.avatar_url
- avatar = user.avatar_url
- else
- avatar = gon.default_avatar_url
-
- "<div class='user-result #{'no-username' unless user.username}'>
- <div class='user-image'><img class='avatar s24' src='#{avatar}'></div>
- <div class='user-name'>#{user.name}</div>
- <div class='user-username'>#{user.username || ""}</div>
- </div>"
-
- formatSelection: (user) ->
- user.name
-
- user: (user_id, callback) =>
- url = @buildUrl(@userPath)
- url = url.replace(':id', user_id)
-
- $.ajax(
- url: url
- dataType: "json"
- ).done (user) ->
- callback(user)
-
- # Return users list. Filtered by query
- # Only active users retrieved
- users: (query, callback) =>
- url = @buildUrl(@usersPath)
-
- $.ajax(
- url: url
- data:
- search: query
- per_page: 20
- active: true
- project_id: @projectId
- group_id: @groupId
- current_user: @showCurrentUser
- author_id: @authorId
- dataType: "json"
- ).done (users) ->
- callback(users)
-
- buildUrl: (url) ->
- url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
- return url
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
new file mode 100644
index 00000000000..35401231fbf
--- /dev/null
+++ b/app/assets/javascripts/wikis.js
@@ -0,0 +1,37 @@
+
+/*= 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.coffee b/app/assets/javascripts/wikis.js.coffee
deleted file mode 100644
index 1ee827f1fa3..00000000000
--- a/app/assets/javascripts/wikis.js.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-#= require latinise
-
-class @Wikis
- constructor: ->
- $('.new-wiki-page').on 'submit', (e) =>
- $('[data-error~=slug]').addClass('hidden')
- field = $('#new_wiki_path')
- slug = @slugify(field.val())
-
- if (slug.length > 0)
- path = field.attr('data-wikis-path')
- location.href = path + '/' + slug
- e.preventDefault()
-
- dasherize: (value) ->
- value.replace(/[_\s]+/g, '-')
-
- slugify: (value) =>
- @dasherize(value.trim().toLowerCase().latinise())
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
new file mode 100644
index 00000000000..71236c6a27d
--- /dev/null
+++ b/app/assets/javascripts/zen_mode.js
@@ -0,0 +1,80 @@
+
+/*= provides zen_mode:enter */
+
+
+/*= provides zen_mode:leave */
+
+
+/*= require jquery.scrollTo */
+
+
+/*= require dropzone */
+
+
+/*= require mousetrap */
+
+
+/*= require mousetrap/pause */
+
+(function() {
+ this.ZenMode = (function() {
+ function ZenMode() {
+ this.active_backdrop = null;
+ this.active_textarea = null;
+ $(document).on('click', '.js-zen-enter', function(e) {
+ e.preventDefault();
+ return $(e.currentTarget).trigger('zen_mode:enter');
+ });
+ $(document).on('click', '.js-zen-leave', function(e) {
+ e.preventDefault();
+ return $(e.currentTarget).trigger('zen_mode:leave');
+ });
+ $(document).on('zen_mode:enter', (function(_this) {
+ return function(e) {
+ return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop'));
+ };
+ })(this));
+ $(document).on('zen_mode:leave', (function(_this) {
+ return function(e) {
+ return _this.exit();
+ };
+ })(this));
+ $(document).on('keydown', function(e) {
+ if (e.keyCode === 27) {
+ e.preventDefault();
+ return $(document).trigger('zen_mode:leave');
+ }
+ });
+ }
+
+ ZenMode.prototype.enter = function(backdrop) {
+ Mousetrap.pause();
+ this.active_backdrop = $(backdrop);
+ this.active_backdrop.addClass('fullscreen');
+ this.active_textarea = this.active_backdrop.find('textarea');
+ this.active_textarea.removeAttr('style');
+ return this.active_textarea.focus();
+ };
+
+ ZenMode.prototype.exit = function() {
+ if (this.active_textarea) {
+ Mousetrap.unpause();
+ this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
+ this.scrollTo(this.active_textarea);
+ this.active_textarea = null;
+ this.active_backdrop = null;
+ return Dropzone.forElement('.div-dropzone').enable();
+ }
+ };
+
+ ZenMode.prototype.scrollTo = function(zen_area) {
+ return $.scrollTo(zen_area, 0, {
+ offset: -150
+ });
+ };
+
+ return ZenMode;
+
+ })();
+
+}).call(this);
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
deleted file mode 100644
index 99f35ecfb0f..00000000000
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ /dev/null
@@ -1,80 +0,0 @@
-# Zen Mode (full screen) textarea
-#
-#= provides zen_mode:enter
-#= provides zen_mode:leave
-#
-#= require jquery.scrollTo
-#= require dropzone
-#= require mousetrap
-#= require mousetrap/pause
-#
-# ### Events
-#
-# `zen_mode:enter`
-#
-# Fired when the "Edit in fullscreen" link is clicked.
-#
-# **Synchronicity** Sync
-# **Bubbles** Yes
-# **Cancelable** No
-# **Target** a.js-zen-enter
-#
-# `zen_mode:leave`
-#
-# Fired when the "Leave Fullscreen" link is clicked.
-#
-# **Synchronicity** Sync
-# **Bubbles** Yes
-# **Cancelable** No
-# **Target** a.js-zen-leave
-#
-class @ZenMode
- constructor: ->
- @active_backdrop = null
- @active_textarea = null
-
- $(document).on 'click', '.js-zen-enter', (e) ->
- e.preventDefault()
- $(e.currentTarget).trigger('zen_mode:enter')
-
- $(document).on 'click', '.js-zen-leave', (e) ->
- e.preventDefault()
- $(e.currentTarget).trigger('zen_mode:leave')
-
- $(document).on 'zen_mode:enter', (e) =>
- @enter($(e.target).closest('.md-area').find('.zen-backdrop'))
- $(document).on 'zen_mode:leave', (e) =>
- @exit()
-
- $(document).on 'keydown', (e) ->
- if e.keyCode == 27 # Esc
- e.preventDefault()
- $(document).trigger('zen_mode:leave')
-
- enter: (backdrop) ->
- Mousetrap.pause()
-
- @active_backdrop = $(backdrop)
- @active_backdrop.addClass('fullscreen')
-
- @active_textarea = @active_backdrop.find('textarea')
-
- # Prevent a user-resized textarea from persisting to fullscreen
- @active_textarea.removeAttr('style')
- @active_textarea.focus()
-
- exit: ->
- if @active_textarea
- Mousetrap.unpause()
-
- @active_textarea.closest('.zen-backdrop').removeClass('fullscreen')
-
- @scrollTo(@active_textarea)
-
- @active_textarea = null
- @active_backdrop = null
-
- Dropzone.forElement('.div-dropzone').enable()
-
- scrollTo: (zen_area) ->
- $.scrollTo(zen_area, 0, offset: -150)
diff --git a/app/assets/stylesheets/behaviors.scss b/app/assets/stylesheets/behaviors.scss
index 542a53f0377..897bc49e7df 100644
--- a/app/assets/stylesheets/behaviors.scss
+++ b/app/assets/stylesheets/behaviors.scss
@@ -20,3 +20,8 @@
.turn-off { display: block; }
}
}
+
+// Hide element if Vue is still working on rendering it fully.
+[v-cloak="true"] {
+ display: none !important;
+}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index a306b8f3f29..d5cca1b10fb 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
@import "framework/issue_box.scss";
@import "framework/jquery.scss";
@import "framework/lists.scss";
+@import "framework/logo.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/modal.scss";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index bb8d71fbae8..c79b22d4d21 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -5,6 +5,7 @@
height: 40px;
padding: 0;
@include border-radius($avatar_radius);
+ border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
float: none;
@@ -15,11 +16,13 @@
&.s24 { margin-right: 4px; }
}
- &.group-avatar, &.project-avatar, &.avatar-tile {
+ &.avatar-tile {
@include border-radius(0);
+ border: none;
}
&.s16 { width: 16px; height: 16px; margin-right: 6px; }
+ &.s20 { width: 20px; height: 20px; margin-right: 7px; }
&.s24 { width: 24px; height: 24px; margin-right: 8px; }
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
@@ -42,12 +45,12 @@
&.s16 { font-size: 12px; line-height: 1.33; }
&.s24 { font-size: 14px; line-height: 1.8; }
&.s26 { font-size: 20px; line-height: 1.33; }
- &.s32 { font-size: 20px; line-height: 32px; }
- &.s40 { font-size: 16px; line-height: 40px; }
- &.s60 { font-size: 32px; line-height: 60px; }
- &.s70 { font-size: 34px; line-height: 70px; }
- &.s90 { font-size: 36px; line-height: 90px; }
- &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
- &.s140 { font-size: 72px; line-height: 140px; }
- &.s160 { font-size: 96px; line-height: 160px; }
+ &.s32 { font-size: 20px; line-height: 30px; }
+ &.s40 { font-size: 16px; line-height: 38px; }
+ &.s60 { font-size: 32px; line-height: 58px; }
+ &.s70 { font-size: 34px; line-height: 68px; }
+ &.s90 { font-size: 36px; line-height: 88px; }
+ &.s110 { font-size: 40px; line-height: 108px; font-weight: 300; }
+ &.s140 { font-size: 72px; line-height: 138px; }
+ &.s160 { font-size: 96px; line-height: 158px; }
}
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
index d28cda6d62d..540718197e0 100644
--- a/app/assets/stylesheets/framework/blank.scss
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -20,9 +20,12 @@
.blank-state-icon {
padding-bottom: 20px;
+ color: $gray-darkest;
+ font-size: 56px;
- path {
- fill: $gray-darkest;
+ path,
+ polygon {
+ fill: currentColor;
}
}
@@ -37,6 +40,10 @@
margin-top: 0;
margin-bottom: $gl-padding;
font-size: 15px;
+
+ > strong {
+ font-weight: 600;
+ }
}
.blank-state-welcome-title {
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 41e77a4ac68..7ce203d2ec7 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -16,6 +16,15 @@
font-weight: normal;
font-size: 16px;
line-height: 36px;
+
+ &.diff-collapsed {
+ padding: 5px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $row-hover;
+ }
+ }
}
.row-content-block {
@@ -223,7 +232,9 @@
.nav-block {
.controls {
float: right;
- margin-top: 11px;
+ margin-top: 8px;
+ padding-bottom: 7px;
+ border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 590b8f54363..cd3ddf5fee9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -49,6 +49,17 @@
border-color: $border-dark;
color: $color;
}
+
+ svg {
+
+ path {
+ fill: $color;
+ }
+
+ use {
+ stroke: $color;
+ }
+ }
}
@mixin btn-green {
@@ -124,6 +135,15 @@
@include btn-green;
}
+ &.btn-inverted {
+ &.btn-success,
+ &.btn-new,
+ &.btn-create,
+ &.btn-save {
+ @include btn-outline($white-light, $green-normal, $green-normal, $green-light, $white-light, $green-light);
+ }
+ }
+
&.btn-gray {
@include btn-gray;
}
@@ -144,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
}
+ &.btn-spam {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
+
&.btn-danger,
&.btn-remove,
&.btn-red {
@@ -173,6 +197,17 @@
.caret {
margin-left: 5px;
}
+
+ svg {
+ height: 15px;
+ width: 15px;
+ position: relative;
+ top: 2px;
+ }
+
+ svg, .fa {
+ margin-right: 3px;
+ }
}
.btn-lg {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index f8aecd0558d..8984bce616c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -248,7 +248,7 @@ li.note {
img.emoji {
height: 20px;
- vertical-align: middle;
+ vertical-align: top;
width: 20px;
}
@@ -270,21 +270,6 @@ table {
}
}
-.dashboard-intro-icon {
- float: left;
- text-align: center;
- font-size: 32px;
- color: #aaa;
- width: 60px;
-}
-
-.dashboard-intro-text {
- display: inline-block;
- margin-left: -60px;
- padding-left: 60px;
- width: 100%;
-}
-
.btn-sign-in {
text-shadow: none;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d4e900f80ef..be5c64c56d3 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -17,6 +17,12 @@
.dropdown {
position: relative;
+
+ .btn-link {
+ &:hover {
+ cursor: pointer;
+ }
+ }
}
.open {
@@ -56,9 +62,13 @@
position: absolute;
top: 50%;
right: 6px;
- margin-top: -4px;
+ margin-top: -6px;
color: $dropdown-toggle-icon-color;
font-size: 10px;
+ &.fa-spinner {
+ font-size: 16px;
+ margin-top: -8px;
+ }
}
&:hover, {
@@ -72,6 +82,23 @@
&.large {
width: 200px;
}
+
+ &.wide {
+ width: 100%;
+
+ + .dropdown-select {
+ width: 100%;
+ }
+ }
+
+ // Allows dynamic-width text in the dropdown toggle.
+ // Resizes to allow long text without overflowing the container.
+ &.dynamic {
+ width: auto;
+ min-width: 160px;
+ max-width: 100%;
+ padding-right: 25px;
+ }
}
.dropdown-menu,
@@ -350,6 +377,7 @@
.dropdown-input-field, .default-dropdown-input {
width: 100%;
+ min-height: 30px;
padding: 0 7px;
color: $dropdown-input-color;
line-height: 30px;
@@ -397,6 +425,7 @@
font-size: 14px;
a {
+ cursor: pointer;
padding-left: 10px;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 71e4b50f2af..d3e3fc50736 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -63,14 +63,15 @@
&.image_file {
background: #eee;
text-align: center;
+
img {
- padding: 100px;
- max-width: 50%;
+ padding: 20px;
+ max-width: 80%;
}
}
&.wiki {
- padding: $gl-padding;
+ padding: 30px $gl-padding;
.highlight {
margin-bottom: 9px;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index a951a2b97fe..0c21d0240b3 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -1,8 +1,8 @@
.flash-container {
cursor: pointer;
margin: 0;
+ margin-bottom: $gl-padding;
font-size: 14px;
- width: 100%;
z-index: 100;
.flash-notice {
@@ -18,9 +18,27 @@
}
.flash-notice, .flash-alert {
- .container-fluid.flash-text {
+ border-radius: $border-radius-default;
+
+ .container-fluid.container-limited.flash-text {
background: transparent;
}
}
+
+ &.flash-container-page {
+ margin-bottom: 0;
+
+ .flash-notice, .flash-alert {
+ border-radius: 0;
+ }
+ }
+}
+
+@media (max-width: $screen-md-min) {
+ ul.notes {
+ .flash-container.timeline-content {
+ margin-left: 0;
+ }
+ }
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 43d55661541..37ff7e22ed1 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -19,7 +19,6 @@ input[type='text'].danger {
}
.form-actions {
- margin: -$gl-padding;
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index f4d35c4b4b1..c0de09f3968 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -2,7 +2,7 @@
* Styles that apply to all GFM related forms.
*/
-.gfm-commit, .gfm-commit_range {
+.gfm-commit_range {
font-family: $monospace_font;
font-size: 90%;
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index d1ff63bd099..3673b81f183 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -11,7 +11,6 @@
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
- background: $color;
&:hover {
color: $white-light;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0c607071840..afe4a276ae5 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,16 +2,6 @@
* Application Header
*
*/
-@mixin tanuki-logo-colors($path-color) {
- fill: $path-color;
- transition: all 0.8s;
-
- &:hover,
- &.highlight {
- fill: lighten($path-color, 25%);
- transition: all 0.1s;
- }
-}
header {
transition: padding $sidebar-transition-duration;
@@ -25,7 +15,7 @@ header {
margin: 8px 0;
text-align: center;
- #tanuki-logo, img {
+ .tanuki-logo, img {
height: 36px;
}
}
@@ -205,26 +195,6 @@ header {
}
}
-#tanuki-logo {
-
- #tanuki-left-ear,
- #tanuki-right-ear,
- #tanuki-nose {
- @include tanuki-logo-colors($tanuki-red);
- }
-
- #tanuki-left-eye,
- #tanuki-right-eye {
- @include tanuki-logo-colors($tanuki-orange);
- }
-
- #tanuki-left-cheek,
- #tanuki-right-cheek {
- @include tanuki-logo-colors($tanuki-yellow);
- }
-
-}
-
@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 7cf4d4fba42..07c8874bf03 100644
--- a/app/assets/stylesheets/framework/highlight.scss
+++ b/app/assets/stylesheets/framework/highlight.scss
@@ -6,11 +6,11 @@
table-layout: fixed;
pre {
- padding: 10px;
+ padding: 10px 0;
border: none;
border-radius: 0;
font-family: $monospace_font;
- font-size: $code_font_size !important;
+ font-size: $code_font_size;
line-height: $code_line_height !important;
margin: 0;
overflow: auto;
@@ -20,13 +20,20 @@
border-left: 1px solid;
code {
+ display: inline-block;
+ min-width: 100%;
font-family: $monospace_font;
- white-space: pre;
+ white-space: normal;
word-wrap: normal;
padding: 0;
.line {
- display: inline-block;
+ display: block;
+ width: 100%;
+ min-height: 19px;
+ padding-left: 10px;
+ padding-right: 10px;
+ white-space: pre;
}
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 2c40ec430ca..965fcc06518 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -114,6 +114,12 @@ ul.content-list {
font-size: $list-font-size;
color: $list-text-color;
+ &.no-description {
+ .title {
+ line-height: $list-text-height;
+ }
+ }
+
.title {
font-weight: 600;
}
@@ -134,12 +140,11 @@ ul.content-list {
}
.controls {
- padding-top: 1px;
float: right;
> .control-text {
margin-right: $gl-padding-top;
- line-height: 40px;
+ line-height: $list-text-height;
&:last-child {
margin-right: 0;
@@ -150,7 +155,7 @@ ul.content-list {
> .btn-group {
margin-right: $gl-padding-top;
display: inline-block;
- margin-top: 4px;
+ margin-top: 3px;
margin-bottom: 4px;
&:last-child {
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
new file mode 100644
index 00000000000..3ee3fb4cee5
--- /dev/null
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -0,0 +1,118 @@
+@mixin unique-keyframes {
+ $animation-name: unique-id();
+ @include webkit-prefix(animation-name, $animation-name);
+
+ @-webkit-keyframes #{$animation-name} {
+ @content;
+ }
+ @keyframes #{$animation-name} {
+ @content;
+ }
+}
+
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
+@mixin tanuki-second-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 10%, 80% {
+ fill: #{$tanuki-color}
+ }
+ 20%, 90% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+@mixin tanuki-forth-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 30%, 60% {
+ fill: #{$tanuki-color};
+ }
+ 40%, 70% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+.tanuki-logo {
+
+ .tanuki-left-ear,
+ .tanuki-right-ear,
+ .tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
+ }
+
+ .tanuki-left-eye,
+ .tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
+ }
+
+ .tanuki-left-cheek,
+ .tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
+ }
+
+ &.animate {
+ .tanuki-shape {
+ @include webkit-prefix(animation-duration, 1.5s);
+ @include webkit-prefix(animation-iteration-count, infinite);
+ }
+
+ .tanuki-left-cheek {
+ @include unique-keyframes {
+ 0%, 10%, 100% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ 90% {
+ fill: $tanuki-yellow;
+ }
+ }
+ }
+
+ .tanuki-left-eye {
+ @include tanuki-second-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-left-ear {
+ @include tanuki-second-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-nose {
+ @include unique-keyframes {
+ 20%, 70% {
+ fill: $tanuki-red;
+ }
+ 30%, 80% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
+ }
+
+ .tanuki-right-eye {
+ @include tanuki-forth-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-right-ear {
+ @include tanuki-forth-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-right-cheek {
+ @include unique-keyframes {
+ 40% {
+ fill: $tanuki-yellow;
+ }
+ 60% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5d3273ea64d..edea4ad00eb 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -98,13 +98,30 @@
.md {
&.md-preview-holder {
- code {
- white-space: pre-wrap;
- word-break: keep-all;
- }
-
+ // Reset ul style types since we're nested inside a ul already
@include bulleted-list;
}
+
+ // On diffs code should wrap nicely and not overflow
+ code {
+ white-space: pre-wrap;
+ word-break: keep-all;
+ }
+
+ hr {
+ // Darken 'whitesmoke' a bit to make it more visible in note bodies
+ border-color: darken(#f5f5f5, 8%);
+ margin: 10px 0;
+ }
+
+ // Border around images in issue and MR comments.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px 0;
+ // Ensure that image does not exceed viewport
+ max-height: calc(100vh - 100px);
+ }
}
.toolbar-group {
@@ -130,3 +147,8 @@
color: $gl-link-color;
}
}
+
+.atwho-view small.description {
+ float: right;
+ padding: 3px 5px;
+}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 5ec5a96a597..396a37bab6e 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -9,22 +9,6 @@
border-radius: $radius;
}
-@mixin border-radius-left($radius) {
- @include border-radius($radius 0 0 $radius)
-}
-
-@mixin border-radius-right($radius) {
- @include border-radius(0 0 $radius $radius)
-}
-
-@mixin linear-gradient($from, $to) {
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
- background-image: -webkit-linear-gradient($from, $to);
- background-image: -moz-linear-gradient($from, $to);
- background-image: -ms-linear-gradient($from, $to);
- background-image: -o-linear-gradient($from, $to);
-}
-
@mixin transition($transition) {
-webkit-transition: $transition;
-moz-transition: $transition;
@@ -38,14 +22,6 @@
* Mixins with fixed values
*/
-@mixin shade {
- @include box-shadow(0 0 3px #ddd);
-}
-
-@mixin solid-shade {
- @include box-shadow(0 0 0 3px #f1f1f1);
-}
-
@mixin str-truncated($max_width: 82%) {
display: inline-block;
overflow: hidden;
@@ -94,23 +70,6 @@
}
}
-@mixin input-big {
- height: 36px;
- padding: 5px 10px;
- font-size: 16px;
- line-height: 24px;
- color: #7f8fa4;
- background-color: #fff;
- border-color: #e7e9ed;
-}
-
-@mixin btn-big {
- height: 36px;
- padding: 5px 10px;
- font-size: 16px;
- line-height: 24px;
-}
-
@mixin bulleted-list {
> ul {
list-style-type: disc;
@@ -123,4 +82,14 @@
}
}
}
-} \ No newline at end of file
+}
+
+@mixin dark-diff-match-line {
+ color: rgba(255, 255, 255, 0.3);
+ background: rgba(255, 255, 255, 0.1);
+}
+
+@mixin webkit-prefix($property, $value) {
+ #{'-webkit-' + $property}: $value;
+ #{$property}: $value;
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 26ad2870aa0..8374f30d0b2 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,5 @@
.modal-body {
position: relative;
- overflow-y: auto;
padding: 15px;
.form-actions {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 364952d3b4a..ef2fe844f94 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,4 +1,4 @@
-@mixin fade($gradient-direction, $rgba, $gradient-color) {
+@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 2;
@@ -8,10 +8,10 @@
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
- background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
+ background: -o-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
+ background: -moz-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
+ background: linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
&.scrolling {
visibility: visible;
@@ -71,7 +71,8 @@
.badge {
font-weight: normal;
background-color: #eee;
- color: #78a;
+ color: $btn-transparent-color;
+ vertical-align: baseline;
}
}
@@ -160,6 +161,7 @@
> .dropdown {
margin-right: $gl-padding-top;
display: inline-block;
+ vertical-align: top;
&:last-child {
margin-right: 0;
@@ -182,7 +184,6 @@
> form {
display: inline-block;
- margin-top: -1px;
}
.icon-label {
@@ -193,7 +194,6 @@
height: 35px;
display: inline-block;
position: relative;
- top: 2px;
margin-right: $gl-padding-top;
/* Medium devices (desktops, 992px and up) */
@@ -336,10 +336,6 @@
}
}
- .badge {
- color: $gl-icon-color;
- }
-
&:hover {
a, i {
color: $black;
@@ -357,7 +353,7 @@
}
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(left, $background-color);
right: -5px;
.fa {
@@ -366,7 +362,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(right, $background-color);
left: -5px;
.fa {
@@ -377,6 +373,7 @@
&.sub-nav-scroll {
.fade-right {
+ @include fade(left, $dark-background-color);
right: 0;
.fa {
@@ -385,6 +382,7 @@
}
.fade-left {
+ @include fade(right, $dark-background-color);
left: 0;
.fa {
@@ -401,7 +399,7 @@
@include scrolling-links();
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(left, $white-light);
right: -5px;
.fa {
@@ -410,7 +408,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(right, $white-light);
left: -5px;
.fa {
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index 874416e1007..c6f30e144fd 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -23,4 +23,9 @@
margin-top: $gl-padding;
}
}
+
+ .panel-title {
+ font-size: inherit;
+ line-height: inherit;
+ }
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 21d87cc9d34..b2e22b60440 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -45,7 +45,8 @@
min-width: 175px;
}
-.select2-results .select2-result-label {
+.select2-results .select2-result-label,
+.select2-more-results {
padding: 10px 15px;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 188823054fd..015fe3debf9 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -3,6 +3,12 @@
padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
+ &.page-sidebar-pinned {
+ .sidebar-wrapper {
+ @include box-shadow(none);
+ }
+ }
+
.sidebar-wrapper {
position: fixed;
top: 0;
@@ -11,6 +17,7 @@
height: 100%;
overflow: hidden;
transition: width $sidebar-transition-duration;
+ @include box-shadow(2px 0 16px 0 $black-transparent);
}
}
@@ -48,10 +55,6 @@
overflow-y: auto;
overflow-x: hidden;
- @media (min-width: $sidebar-breakpoint) {
- bottom: 50px;
- }
-
&.navbar-collapse {
padding: 0 !important;
}
@@ -69,7 +72,7 @@
}
a {
- padding: 7px 15px 7px 12px;
+ padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
@@ -101,7 +104,7 @@
}
}
-.toggle-nav-collapse {
+.sidebar-action-buttons {
width: $sidebar_width;
position: absolute;
top: 0;
@@ -110,12 +113,37 @@
padding: 5px 0;
font-size: 18px;
line-height: 30px;
+
+ .toggle-nav-collapse {
+ left: 0;
+ }
+
+ .pin-nav-btn {
+ right: 0;
+ display: none;
+
+ @media (min-width: $sidebar-breakpoint) {
+ display: block;
+ }
+
+ .fa {
+ transition: transform .15s;
+ }
+
+ &.is-active {
+ .fa {
+ transform: rotate(90deg);
+ }
+ }
+ }
}
.nav-header-btn {
- padding: 10px 5px;
+ padding: 10px $gl-sidebar-padding;
color: inherit;
transition-duration: .3s;
+ position: absolute;
+ top: 0;
&:hover,
&:focus {
@@ -124,30 +152,6 @@
}
}
-.pin-nav-btn {
- display: none;
- position: absolute;
- left: 0;
- bottom: 0;
- height: 50px;
- width: $sidebar_width;
- line-height: 30px;
-
- @media (min-width: $sidebar-breakpoint) {
- display: block;
- }
-
- .fa {
- transition: transform .15s;
- }
-
- &.is-active {
- .fa {
- transform: rotate(90deg);
- }
- }
-}
-
.page-sidebar-collapsed {
padding-left: 0;
@@ -194,6 +198,10 @@ header.header-pinned-nav {
.sidebar-collapsed-icon {
cursor: pointer;
+
+ .btn {
+ background-color: $gray-light;
+ }
}
}
@@ -214,3 +222,7 @@ header.header-pinned-nav {
padding-right: $sidebar_collapsed_width;
}
}
+
+.right-sidebar {
+ border-left: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 3575984b229..06874a993fa 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -14,12 +14,20 @@
margin-top: 0;
}
+ // Single code lines should wrap
code {
font-family: $monospace_font;
- white-space: pre;
+ white-space: pre-wrap;
word-wrap: normal;
}
+ // Multi-line code blocks should scroll horizontally
+ pre {
+ code {
+ white-space: pre;
+ }
+ }
+
kbd {
display: inline-block;
padding: 3px 5px;
@@ -37,39 +45,41 @@
}
h1 {
- font-size: 1.3em;
+ font-size: 2em;
font-weight: 600;
- margin: 24px 0 12px;
- padding: 0 0 10px;
- border-bottom: 1px solid #e7e9ed;
+ margin: 1em 0 10px;
+ padding: 0 0 0.3em;
+ border-bottom: 1px solid $btn-default-border;
color: $gl-gray-dark;
}
h2 {
- font-size: 1.2em;
+ font-size: 1.6em;
font-weight: 600;
- margin: 24px 0 12px;
+ margin: 1em 0 10px;
+ padding-bottom: 0.3em;
+ border-bottom: 1px solid $btn-default-border;
color: $gl-gray-dark;
}
h3 {
- margin: 24px 0 12px;
- font-size: 1.1em;
+ margin: 1em 0 10px;
+ font-size: 1.4em;
}
h4 {
- margin: 24px 0 12px;
- font-size: 0.98em;
+ margin: 1em 0 10px;
+ font-size: 1.25em;
}
h5 {
- margin: 24px 0 12px;
- font-size: 0.95em;
+ margin: 1em 0 10px;
+ font-size: 1em;
}
h6 {
- margin: 24px 0 12px;
- font-size: 0.90em;
+ margin: 1em 0 10px;
+ font-size: 0.95em;
}
blockquote {
@@ -115,7 +125,7 @@
ul, ol {
padding: 0;
- margin: 6px 0 6px 28px !important;
+ margin: 3px 0 3px 28px !important;
}
li {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4337fab5d87..5da390118c6 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -16,7 +16,8 @@ $border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$table-border-color: #f0f0f0;
$background-color: #fafafa;
-$dark-background-color: #f7f7f7;
+$dark-background-color: #f5f5f5;
+$table-text-gray: #8f8f8f;
/*
* Text
@@ -42,6 +43,7 @@ $gl-header-color: $gl-title-color;
$list-font-size: $gl-font-size;
$list-title-color: $gl-title-color;
$list-text-color: $gl-text-color;
+$list-text-height: 42px;
/*
* Markdown
@@ -63,6 +65,7 @@ $gl-btn-padding: 10px;
$gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
+$gl-sidebar-padding: 22px;
/*
* Misc
@@ -273,3 +276,5 @@ $personal-access-tokens-disabled-label-color: #bbb;
$ci-output-bg: #1d1f21;
$ci-text-color: #c5c8c6;
+
+$issue-boards-font-size: 15px;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 77a73dc379b..16ffbe57a99 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #557;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 80a509a7c1a..7de920e074b 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #49483e;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index c62bd021aef..b11499c71ee 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -21,6 +21,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include dark-diff-match-line;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #174652;
@@ -36,8 +40,7 @@
}
.line_content.match {
- color: rgba(255, 255, 255, 0.3);
- background: rgba(255, 255, 255, 0.1);
+ @include dark-diff-match-line;
}
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 524cfaf90c3..657bb5e3cd9 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,4 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background: rgba(255, 255, 255, 0.4);
+}
+
.code.solarized-light {
// Line numbers
.line-numbers, .diff-line-num {
@@ -21,6 +27,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
td.diff-line-num.hll:not(.empty-cell),
td.line_content.hll:not(.empty-cell) {
background-color: #ddd8c5;
@@ -36,8 +46,7 @@
}
.line_content.match {
- color: $black-transparent;
- background: rgba(255, 255, 255, 0.4);
+ @include matchLine;
}
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 31a4e3deaac..36a80a916b2 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,4 +1,10 @@
/* https://github.com/aahan/pygments-github-style */
+
+@mixin matchLine {
+ color: $black-transparent;
+ background-color: $match-line;
+}
+
.code.white {
// Line numbers
.line-numbers, .diff-line-num {
@@ -22,6 +28,10 @@
// Diff line
.line_holder {
+ &.match .line_content {
+ @include matchLine;
+ }
+
.diff-line-num {
&.old {
background-color: $line-number-old;
@@ -57,8 +67,7 @@
}
&.match {
- color: $black-transparent;
- background-color: $match-line;
+ @include matchLine;
}
&.hll:not(.empty-cell) {
diff --git a/app/assets/stylesheets/mailers/repository_push_email.scss b/app/assets/stylesheets/mailers/repository_push_email.scss
index 7f645d3089d..5bfe9bcb443 100644
--- a/app/assets/stylesheets/mailers/repository_push_email.scss
+++ b/app/assets/stylesheets/mailers/repository_push_email.scss
@@ -10,83 +10,47 @@
// preference): plain class selectors, type (element name) selectors, or
// explicit child selectors.
-table.code {
- width: 100%;
+.code {
+ background-color: #fff;
font-family: monospace;
- border: none;
- border-collapse: separate;
- margin: 0;
- padding: 0;
+ font-size: $code_font_size;
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
-premailer-width: 100%;
- > tr > td {
+ > tr {
line-height: $code_line_height;
- font-family: monospace;
- font-size: $code_font_size;
-
- &.diff-line-num {
- margin: 0;
- padding: 0;
- border: none;
- padding: 0 5px;
- border-right: 1px solid;
- text-align: right;
- min-width: 35px;
- max-width: 50px;
- width: 35px;
- }
-
- &.line_content {
- display: block;
- margin: 0;
- padding: 0 0.5em;
- border: none;
- white-space: pre;
- }
}
}
-.line-numbers, .diff-line-num {
+.diff-line-num {
+ padding: 0 5px;
+ text-align: right;
+ width: 35px;
background-color: $background-color;
-}
-
-.diff-line-num, .diff-line-num a {
color: $black-transparent;
-}
-
-pre.code, .diff-line-num {
- border-color: $table-border-gray;
-}
-
-.code.white, pre.code, .line_content {
- background-color: #fff;
- color: #333;
-}
+ border-right: 1px solid $table-border-gray;
-.diff-line-num {
&.old {
background-color: $line-number-old;
- border-color: $line-removed-dark;
+ border-right-color: $line-removed-dark;
}
&.new {
background-color: $line-number-new;
- border-color: $line-added-dark;
- }
-
- &.hll:not(.empty-cell) {
- background-color: $line-number-select;
- border-color: $line-select-yellow-dark;
+ 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 {
+ > .line > span.idiff,
+ > .line > span > span.idiff {
background-color: $line-removed-dark;
}
}
@@ -94,7 +58,8 @@ pre.code, .diff-line-num {
&.new {
background-color: $line-added;
- > .line > span.idiff, > .line > span > span.idiff {
+ > .line > span.idiff,
+ > .line > span > span.idiff {
background-color: $line-added-dark;
}
}
@@ -103,14 +68,10 @@ pre.code, .diff-line-num {
color: $black-transparent;
background-color: $match-line;
}
-
- &.hll:not(.empty-cell) {
- background-color: $line-select-yellow;
- }
}
-pre > .hll {
- background-color: #f8eec7 !important;
+pre {
+ margin: 0;
}
span.highlight_word {
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 1d34a7f79ae..8f71381f5c4 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -72,7 +72,6 @@
margin-bottom: 20px;
}
-
// Users List
.users-list {
@@ -88,13 +87,7 @@
.user-name {
display: inline-block;
- font-weight: bold;
- }
-
- .controls {
- > .btn, > .dropdown {
- margin-left: 5px;
- }
+ font-weight: 600;
}
.dropdown {
@@ -103,4 +96,49 @@
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/boards.scss b/app/assets/stylesheets/pages/boards.scss
new file mode 100644
index 00000000000..9ac4d801ac4
--- /dev/null
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -0,0 +1,306 @@
+[v-cloak] {
+ display: none;
+}
+
+.user-can-drag {
+ cursor: -webkit-grab;
+ cursor: grab;
+}
+
+.is-dragging {
+ // Important because plugin sets inline CSS
+ opacity: 1!important;
+
+ * {
+ // !important to make sure no style can override this when dragging
+ cursor: -webkit-grabbing!important;
+ cursor: grabbing!important;
+ }
+}
+
+.dropdown-menu-issues-board-new {
+ width: 320px;
+
+ .dropdown-content {
+ max-height: 150px;
+ }
+}
+
+.issue-board-dropdown-content {
+ margin: 0 8px 10px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid $dropdown-divider-color;
+
+ > p {
+ margin: 0;
+ font-size: 14px;
+ color: #9c9c9c;
+ }
+}
+
+.issue-boards-page {
+ .content-wrapper {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ }
+
+ .sub-nav,
+ .issues-filters {
+ -webkit-flex: none;
+ flex: none;
+ }
+
+ .page-with-sidebar {
+ display: -webkit-flex;
+ display: flex;
+ min-height: 100vh;
+ max-height: 100vh;
+ padding-bottom: 0;
+ }
+
+ .issue-boards-content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ width: 100%;
+
+ .content {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ }
+ }
+}
+
+.boards-app-loading {
+ width: 100%;
+ font-size: 34px;
+}
+
+.boards-list {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex: 1;
+ flex: 1;
+ -webkit-flex-basis: 0;
+ flex-basis: 0;
+ min-height: calc(100vh - 152px);
+ max-height: calc(100vh - 152px);
+ padding-top: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+ overflow-x: scroll;
+
+ @media (min-width: $screen-sm-min) {
+ min-height: 475px;
+ max-height: none;
+ }
+}
+
+.board {
+ display: -webkit-flex;
+ display: flex;
+ min-width: calc(85vw - 15px);
+ max-width: calc(85vw - 15px);
+ margin-bottom: 25px;
+ padding-right: ($gl-padding / 2);
+ padding-left: ($gl-padding / 2);
+
+ @media (min-width: $screen-sm-min) {
+ min-width: 400px;
+ max-width: 400px;
+ }
+}
+
+.board-inner {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ width: 100%;
+ font-size: $issue-boards-font-size;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
+.board-header {
+ border-top-left-radius: $border-radius-default;
+ border-top-right-radius: $border-radius-default;
+
+ &.has-border {
+ border-top: 3px solid;
+
+ .board-title {
+ padding-top: ($gl-padding - 3px);
+ }
+ }
+}
+
+.board-header-loading-spinner {
+ margin-right: 10px;
+ color: $gray-darkest;
+}
+
+.board-inner-container {
+ border-bottom: 1px solid $border-color;
+ padding: $gl-padding;
+}
+
+.board-title {
+ position: relative;
+ margin: 0;
+ padding: $gl-padding;
+ font-size: 1em;
+ border-bottom: 1px solid $border-color;
+}
+
+.board-search-container {
+ position: relative;
+ background-color: #fff;
+
+ .form-control {
+ padding-right: 30px;
+ }
+}
+
+.board-search-icon,
+.board-search-clear-btn {
+ position: absolute;
+ right: $gl-padding + 10px;
+ top: 50%;
+ margin-top: -7px;
+ font-size: 14px;
+}
+
+.board-search-icon {
+ color: $gl-placeholder-color;
+}
+
+.board-search-clear-btn {
+ padding: 0;
+ line-height: 1;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-delete {
+ margin-right: 10px;
+ padding: 0;
+ color: $gray-darkest;
+ background-color: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+}
+
+.board-blank-state {
+ height: 100%;
+ padding: $gl-padding;
+ background-color: #fff;
+}
+
+.board-blank-state-list {
+ list-style: none;
+
+ > li:not(:last-child) {
+ margin-bottom: 8px;
+ }
+
+ .label-color {
+ position: relative;
+ top: 2px;
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ margin-right: 3px;
+ border-radius: $border-radius-default;
+ }
+}
+
+.board-list {
+ -webkit-flex: 1;
+ flex: 1;
+ height: 400px;
+ margin-bottom: 0;
+ padding: 5px;
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.board-list-loading {
+ margin-top: 10px;
+ font-size: 26px;
+}
+
+.is-ghost {
+ opacity: 0.3;
+}
+
+.card {
+ position: relative;
+ width: 100%;
+ padding: 10px $gl-padding;
+ background: #fff;
+ border-radius: $border-radius-default;
+ box-shadow: 0 1px 2px rgba(186, 186, 186, 0.5);
+ list-style: none;
+
+ &.user-can-drag {
+ padding-left: $gl-padding;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .label {
+ border: 0;
+ outline: 0;
+ }
+
+ .confidential-icon {
+ margin-right: 5px;
+ }
+}
+
+.card-title {
+ margin: 0;
+ font-size: 1em;
+
+ a {
+ color: inherit;
+ }
+}
+
+.card-footer {
+ margin-top: 5px;
+ line-height: 25px;
+
+ .label {
+ margin-right: 4px;
+ font-size: (14px / $issue-boards-font-size) * 1em;
+ }
+}
+
+.card-number {
+ margin-right: 8px;
+ font-weight: 500;
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e8f1935d239..8c33e7d9a2e 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -83,14 +83,6 @@
}
}
-table.builds {
- .build-link {
- a {
- color: $gl-dark-link-color;
- }
- }
-}
-
.build-trace {
background: $ci-output-bg;
color: $ci-text-color;
@@ -108,24 +100,101 @@ table.builds {
}
.right-sidebar.build-sidebar {
- padding-top: $gl-padding;
- padding-bottom: $gl-padding;
+ padding: $gl-padding 0;
&.right-sidebar-collapsed {
display: none;
}
+ .blocks-container {
+ padding: $gl-padding;
+ }
+
.block {
width: 100%;
}
.build-sidebar-header {
- padding-top: 0;
+ padding: 0 $gl-padding $gl-padding;
.gutter-toggle {
margin-top: 0;
}
}
+
+ .stage-item {
+ cursor: pointer;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ .build-dropdown {
+ padding: 0 $gl-padding;
+
+ .dropdown-menu-toggle {
+ margin-top: 8px;
+ }
+
+ .dropdown-menu {
+ right: $gl-padding;
+ left: $gl-padding;
+ width: auto;
+ }
+ }
+
+ .builds-container {
+ margin-top: $gl-padding;
+ background-color: $white-light;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ max-height: 300px;
+ overflow: auto;
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 3px;
+ height: 13px;
+ }
+
+ a {
+ display: block;
+ padding: $gl-padding 10px $gl-padding 40px;
+ width: 270px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ .build-job {
+ position: relative;
+
+ .fa {
+ position: absolute;
+ left: 15px;
+ top: 20px;
+ display: none;
+ }
+
+ &.active {
+ font-weight: bold;
+
+ .fa {
+ display: block;
+ }
+ }
+
+ &:hover {
+ background-color: $row-hover;
+ }
+ }
+ }
}
.build-detail-row {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 35ab28b3fea..53ec0002afe 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -66,6 +66,21 @@
margin-left: 8px;
}
}
+
+ .ci-status-link {
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
+}
+
+.ci-status-link {
+ svg {
+ overflow: visible;
+ }
}
.commit-box {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 85bbf70e188..6a58b445afa 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,8 +1,6 @@
.commits-compare-switch {
@include btn-default;
@include btn-white;
- background: image-url("switch_icon.png") no-repeat center center;
- text-indent: -9999px;
float: left;
margin-right: 9px;
}
@@ -61,7 +59,11 @@
font-size: 0;
}
- .btn-transparent {
+ .ci-status-link {
+ display: inline-block;
+ }
+
+ .btn-clipboard, .btn-transparent {
padding-left: 0;
padding-right: 0;
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index cf7567513ec..42928ee279c 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -36,10 +36,6 @@
.dash-project-avatar {
float: left;
-
- .avatar {
- @include border-radius(50%);
- }
}
.dash-project-access-icon {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 1b389d83525..4d9c73c6840 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -34,11 +34,4 @@
}
}
}
-
- .wiki {
- code {
- white-space: pre-wrap;
- word-break: keep-all;
- }
- }
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 21b1c223c88..21cee2e3a70 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -164,7 +164,10 @@
line-height: 0;
img {
border: 1px solid #fff;
- background: image-url('trans_bg.gif');
+ 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%);
+ background-size: 10px 10px;
+ background-position: 0 0, 5px 5px;
max-width: 100%;
}
&.deleted {
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
index b731abc7450..f17797b2381 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -1,4 +1,4 @@
-.emoji-0023-20E3 { background-position: 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; }
@@ -452,1271 +452,1344 @@
.emoji-1F391 { background-position: -420px -200px; }
.emoji-1F392 { background-position: -420px -220px; }
.emoji-1F393 { background-position: -420px -240px; }
-.emoji-1F394 { background-position: -420px -260px; }
-.emoji-1F395 { background-position: -420px -280px; }
-.emoji-1F396 { background-position: -420px -300px; }
-.emoji-1F397 { background-position: -420px -320px; }
-.emoji-1F398 { background-position: -420px -340px; }
-.emoji-1F399 { background-position: -420px -360px; }
-.emoji-1F39A { background-position: -420px -380px; }
-.emoji-1F39B { background-position: -420px -400px; }
-.emoji-1F39C { background-position: 0 -420px; }
-.emoji-1F39D { background-position: -20px -420px; }
-.emoji-1F39E { background-position: -40px -420px; }
-.emoji-1F39F { background-position: -60px -420px; }
-.emoji-1F3A0 { background-position: -80px -420px; }
-.emoji-1F3A1 { background-position: -100px -420px; }
-.emoji-1F3A2 { background-position: -120px -420px; }
-.emoji-1F3A3 { background-position: -140px -420px; }
-.emoji-1F3A4 { background-position: -160px -420px; }
-.emoji-1F3A5 { background-position: -180px -420px; }
-.emoji-1F3A6 { background-position: -200px -420px; }
-.emoji-1F3A7 { background-position: -220px -420px; }
-.emoji-1F3A8 { background-position: -240px -420px; }
-.emoji-1F3A9 { background-position: -260px -420px; }
-.emoji-1F3AA { background-position: -280px -420px; }
-.emoji-1F3AB { background-position: -300px -420px; }
-.emoji-1F3AC { background-position: -320px -420px; }
-.emoji-1F3AD { background-position: -340px -420px; }
-.emoji-1F3AE { background-position: -360px -420px; }
-.emoji-1F3AF { background-position: -380px -420px; }
-.emoji-1F3B0 { background-position: -400px -420px; }
-.emoji-1F3B1 { background-position: -420px -420px; }
-.emoji-1F3B2 { background-position: -440px 0; }
-.emoji-1F3B3 { background-position: -440px -20px; }
-.emoji-1F3B4 { background-position: -440px -40px; }
-.emoji-1F3B5 { background-position: -440px -60px; }
-.emoji-1F3B6 { background-position: -440px -80px; }
-.emoji-1F3B7 { background-position: -440px -100px; }
-.emoji-1F3B8 { background-position: -440px -120px; }
-.emoji-1F3B9 { background-position: -440px -140px; }
-.emoji-1F3BA { background-position: -440px -160px; }
-.emoji-1F3BB { background-position: -440px -180px; }
-.emoji-1F3BC { background-position: -440px -200px; }
-.emoji-1F3BD { background-position: -440px -220px; }
-.emoji-1F3BE { background-position: -440px -240px; }
-.emoji-1F3BF { background-position: -440px -260px; }
-.emoji-1F3C0 { background-position: -440px -280px; }
-.emoji-1F3C1 { background-position: -440px -300px; }
-.emoji-1F3C2 { background-position: -440px -320px; }
-.emoji-1F3C3 { background-position: -440px -340px; }
-.emoji-1F3C3-1F3FB { background-position: -440px -360px; }
-.emoji-1F3C3-1F3FC { background-position: -440px -380px; }
-.emoji-1F3C3-1F3FD { background-position: -440px -400px; }
-.emoji-1F3C3-1F3FE { background-position: -440px -420px; }
-.emoji-1F3C3-1F3FF { background-position: 0 -440px; }
-.emoji-1F3C4 { background-position: -20px -440px; }
-.emoji-1F3C4-1F3FB { background-position: -40px -440px; }
-.emoji-1F3C4-1F3FC { background-position: -60px -440px; }
-.emoji-1F3C4-1F3FD { background-position: -80px -440px; }
-.emoji-1F3C4-1F3FE { background-position: -100px -440px; }
-.emoji-1F3C4-1F3FF { background-position: -120px -440px; }
-.emoji-1F3C5 { background-position: -140px -440px; }
-.emoji-1F3C6 { background-position: -160px -440px; }
-.emoji-1F3C7 { background-position: -180px -440px; }
-.emoji-1F3C7-1F3FB { background-position: -200px -440px; }
-.emoji-1F3C7-1F3FC { background-position: -220px -440px; }
-.emoji-1F3C7-1F3FD { background-position: -240px -440px; }
-.emoji-1F3C7-1F3FE { background-position: -260px -440px; }
-.emoji-1F3C7-1F3FF { background-position: -280px -440px; }
-.emoji-1F3C8 { background-position: -300px -440px; }
-.emoji-1F3C9 { background-position: -320px -440px; }
-.emoji-1F3CA { background-position: -340px -440px; }
-.emoji-1F3CA-1F3FB { background-position: -360px -440px; }
-.emoji-1F3CA-1F3FC { background-position: -380px -440px; }
-.emoji-1F3CA-1F3FD { background-position: -400px -440px; }
-.emoji-1F3CA-1F3FE { background-position: -420px -440px; }
-.emoji-1F3CA-1F3FF { background-position: -440px -440px; }
-.emoji-1F3CB { background-position: -460px 0; }
-.emoji-1F3CB-1F3FB { background-position: -460px -20px; }
-.emoji-1F3CB-1F3FC { background-position: -460px -40px; }
-.emoji-1F3CB-1F3FD { background-position: -460px -60px; }
-.emoji-1F3CB-1F3FE { background-position: -460px -80px; }
-.emoji-1F3CB-1F3FF { background-position: -460px -100px; }
-.emoji-1F3CC { background-position: -460px -120px; }
-.emoji-1F3CD { background-position: -460px -140px; }
-.emoji-1F3CE { background-position: -460px -160px; }
-.emoji-1F3CF { background-position: -460px -180px; }
-.emoji-1F3D0 { background-position: -460px -200px; }
-.emoji-1F3D1 { background-position: -460px -220px; }
-.emoji-1F3D2 { background-position: -460px -240px; }
-.emoji-1F3D3 { background-position: -460px -260px; }
-.emoji-1F3D4 { background-position: -460px -280px; }
-.emoji-1F3D5 { background-position: -460px -300px; }
-.emoji-1F3D6 { background-position: -460px -320px; }
-.emoji-1F3D7 { background-position: -460px -340px; }
-.emoji-1F3D8 { background-position: -460px -360px; }
-.emoji-1F3D9 { background-position: -460px -380px; }
-.emoji-1F3DA { background-position: -460px -400px; }
-.emoji-1F3DB { background-position: -460px -420px; }
-.emoji-1F3DC { background-position: -460px -440px; }
-.emoji-1F3DD { background-position: 0 -460px; }
-.emoji-1F3DE { background-position: -20px -460px; }
-.emoji-1F3DF { background-position: -40px -460px; }
-.emoji-1F3E0 { background-position: -60px -460px; }
-.emoji-1F3E1 { background-position: -80px -460px; }
-.emoji-1F3E2 { background-position: -100px -460px; }
-.emoji-1F3E3 { background-position: -120px -460px; }
-.emoji-1F3E4 { background-position: -140px -460px; }
-.emoji-1F3E5 { background-position: -160px -460px; }
-.emoji-1F3E6 { background-position: -180px -460px; }
-.emoji-1F3E7 { background-position: -200px -460px; }
-.emoji-1F3E8 { background-position: -220px -460px; }
-.emoji-1F3E9 { background-position: -240px -460px; }
-.emoji-1F3EA { background-position: -260px -460px; }
-.emoji-1F3EB { background-position: -280px -460px; }
-.emoji-1F3EC { background-position: -300px -460px; }
-.emoji-1F3ED { background-position: -320px -460px; }
-.emoji-1F3EE { background-position: -340px -460px; }
-.emoji-1F3EF { background-position: -360px -460px; }
-.emoji-1F3F0 { background-position: -380px -460px; }
-.emoji-1F3F1 { background-position: -400px -460px; }
-.emoji-1F3F2 { background-position: -420px -460px; }
-.emoji-1F3F3 { background-position: -440px -460px; }
-.emoji-1F3F4 { background-position: -460px -460px; }
-.emoji-1F3F5 { background-position: -480px 0; }
-.emoji-1F3F6 { background-position: -480px -20px; }
-.emoji-1F3F7 { background-position: -480px -40px; }
-.emoji-1F3F8 { background-position: -480px -60px; }
-.emoji-1F3F9 { background-position: -480px -80px; }
-.emoji-1F3FA { background-position: -480px -100px; }
-.emoji-1F3FB { background-position: -480px -120px; }
-.emoji-1F3FC { background-position: -480px -140px; }
-.emoji-1F3FD { background-position: -480px -160px; }
-.emoji-1F3FE { background-position: -480px -180px; }
-.emoji-1F3FF { background-position: -480px -200px; }
-.emoji-1F400 { background-position: -480px -220px; }
-.emoji-1F401 { background-position: -480px -240px; }
-.emoji-1F402 { background-position: -480px -260px; }
-.emoji-1F403 { background-position: -480px -280px; }
-.emoji-1F404 { background-position: -480px -300px; }
-.emoji-1F405 { background-position: -480px -320px; }
-.emoji-1F406 { background-position: -480px -340px; }
-.emoji-1F407 { background-position: -480px -360px; }
-.emoji-1F408 { background-position: -480px -380px; }
-.emoji-1F409 { background-position: -480px -400px; }
-.emoji-1F40A { background-position: -480px -420px; }
-.emoji-1F40B { background-position: -480px -440px; }
-.emoji-1F40C { background-position: -480px -460px; }
-.emoji-1F40D { background-position: 0 -480px; }
-.emoji-1F40E { background-position: -20px -480px; }
-.emoji-1F40F { background-position: -40px -480px; }
-.emoji-1F410 { background-position: -60px -480px; }
-.emoji-1F411 { background-position: -80px -480px; }
-.emoji-1F412 { background-position: -100px -480px; }
-.emoji-1F413 { background-position: -120px -480px; }
-.emoji-1F414 { background-position: -140px -480px; }
-.emoji-1F415 { background-position: -160px -480px; }
-.emoji-1F416 { background-position: -180px -480px; }
-.emoji-1F417 { background-position: -200px -480px; }
-.emoji-1F418 { background-position: -220px -480px; }
-.emoji-1F419 { background-position: -240px -480px; }
-.emoji-1F41A { background-position: -260px -480px; }
-.emoji-1F41B { background-position: -280px -480px; }
-.emoji-1F41C { background-position: -300px -480px; }
-.emoji-1F41D { background-position: -320px -480px; }
-.emoji-1F41E { background-position: -340px -480px; }
-.emoji-1F41F { background-position: -360px -480px; }
-.emoji-1F420 { background-position: -380px -480px; }
-.emoji-1F421 { background-position: -400px -480px; }
-.emoji-1F422 { background-position: -420px -480px; }
-.emoji-1F423 { background-position: -440px -480px; }
-.emoji-1F424 { background-position: -460px -480px; }
-.emoji-1F425 { background-position: -480px -480px; }
-.emoji-1F426 { background-position: -500px 0; }
-.emoji-1F427 { background-position: -500px -20px; }
-.emoji-1F428 { background-position: -500px -40px; }
-.emoji-1F429 { background-position: -500px -60px; }
-.emoji-1F42A { background-position: -500px -80px; }
-.emoji-1F42B { background-position: -500px -100px; }
-.emoji-1F42C { background-position: -500px -120px; }
-.emoji-1F42D { background-position: -500px -140px; }
-.emoji-1F42E { background-position: -500px -160px; }
-.emoji-1F42F { background-position: -500px -180px; }
-.emoji-1F430 { background-position: -500px -200px; }
-.emoji-1F431 { background-position: -500px -220px; }
-.emoji-1F432 { background-position: -500px -240px; }
-.emoji-1F433 { background-position: -500px -260px; }
-.emoji-1F434 { background-position: -500px -280px; }
-.emoji-1F435 { background-position: -500px -300px; }
-.emoji-1F436 { background-position: -500px -320px; }
-.emoji-1F437 { background-position: -500px -340px; }
-.emoji-1F438 { background-position: -500px -360px; }
-.emoji-1F439 { background-position: -500px -380px; }
-.emoji-1F43A { background-position: -500px -400px; }
-.emoji-1F43B { background-position: -500px -420px; }
-.emoji-1F43C { background-position: -500px -440px; }
-.emoji-1F43D { background-position: -500px -460px; }
-.emoji-1F43E { background-position: -500px -480px; }
-.emoji-1F43F { background-position: 0 -500px; }
-.emoji-1F440 { background-position: -20px -500px; }
-.emoji-1F441 { background-position: -40px -500px; }
-.emoji-1F441-1F5E8 { background-position: -60px -500px; }
-.emoji-1F442 { background-position: -80px -500px; }
-.emoji-1F442-1F3FB { background-position: -100px -500px; }
-.emoji-1F442-1F3FC { background-position: -120px -500px; }
-.emoji-1F442-1F3FD { background-position: -140px -500px; }
-.emoji-1F442-1F3FE { background-position: -160px -500px; }
-.emoji-1F442-1F3FF { background-position: -180px -500px; }
-.emoji-1F443 { background-position: -200px -500px; }
-.emoji-1F443-1F3FB { background-position: -220px -500px; }
-.emoji-1F443-1F3FC { background-position: -240px -500px; }
-.emoji-1F443-1F3FD { background-position: -260px -500px; }
-.emoji-1F443-1F3FE { background-position: -280px -500px; }
-.emoji-1F443-1F3FF { background-position: -300px -500px; }
-.emoji-1F444 { background-position: -320px -500px; }
-.emoji-1F445 { background-position: -340px -500px; }
-.emoji-1F446 { background-position: -360px -500px; }
-.emoji-1F446-1F3FB { background-position: -380px -500px; }
-.emoji-1F446-1F3FC { background-position: -400px -500px; }
-.emoji-1F446-1F3FD { background-position: -420px -500px; }
-.emoji-1F446-1F3FE { background-position: -440px -500px; }
-.emoji-1F446-1F3FF { background-position: -460px -500px; }
-.emoji-1F447 { background-position: -480px -500px; }
-.emoji-1F447-1F3FB { background-position: -500px -500px; }
-.emoji-1F447-1F3FC { background-position: -520px 0; }
-.emoji-1F447-1F3FD { background-position: -520px -20px; }
-.emoji-1F447-1F3FE { background-position: -520px -40px; }
-.emoji-1F447-1F3FF { background-position: -520px -60px; }
-.emoji-1F448 { background-position: -520px -80px; }
-.emoji-1F448-1F3FB { background-position: -520px -100px; }
-.emoji-1F448-1F3FC { background-position: -520px -120px; }
-.emoji-1F448-1F3FD { background-position: -520px -140px; }
-.emoji-1F448-1F3FE { background-position: -520px -160px; }
-.emoji-1F448-1F3FF { background-position: -520px -180px; }
-.emoji-1F449 { background-position: -520px -200px; }
-.emoji-1F449-1F3FB { background-position: -520px -220px; }
-.emoji-1F449-1F3FC { background-position: -520px -240px; }
-.emoji-1F449-1F3FD { background-position: -520px -260px; }
-.emoji-1F449-1F3FE { background-position: -520px -280px; }
-.emoji-1F449-1F3FF { background-position: -520px -300px; }
-.emoji-1F44A { background-position: -520px -320px; }
-.emoji-1F44A-1F3FB { background-position: -520px -340px; }
-.emoji-1F44A-1F3FC { background-position: -520px -360px; }
-.emoji-1F44A-1F3FD { background-position: -520px -380px; }
-.emoji-1F44A-1F3FE { background-position: -520px -400px; }
-.emoji-1F44A-1F3FF { background-position: -520px -420px; }
-.emoji-1F44B { background-position: -520px -440px; }
-.emoji-1F44B-1F3FB { background-position: -520px -460px; }
-.emoji-1F44B-1F3FC { background-position: -520px -480px; }
-.emoji-1F44B-1F3FD { background-position: -520px -500px; }
-.emoji-1F44B-1F3FE { background-position: 0 -520px; }
-.emoji-1F44B-1F3FF { background-position: -20px -520px; }
-.emoji-1F44C { background-position: -40px -520px; }
-.emoji-1F44C-1F3FB { background-position: -60px -520px; }
-.emoji-1F44C-1F3FC { background-position: -80px -520px; }
-.emoji-1F44C-1F3FD { background-position: -100px -520px; }
-.emoji-1F44C-1F3FE { background-position: -120px -520px; }
-.emoji-1F44C-1F3FF { background-position: -140px -520px; }
-.emoji-1F44D { background-position: -160px -520px; }
-.emoji-1F44D-1F3FB { background-position: -180px -520px; }
-.emoji-1F44D-1F3FC { background-position: -200px -520px; }
-.emoji-1F44D-1F3FD { background-position: -220px -520px; }
-.emoji-1F44D-1F3FE { background-position: -240px -520px; }
-.emoji-1F44D-1F3FF { background-position: -260px -520px; }
-.emoji-1F44E { background-position: -280px -520px; }
-.emoji-1F44E-1F3FB { background-position: -300px -520px; }
-.emoji-1F44E-1F3FC { background-position: -320px -520px; }
-.emoji-1F44E-1F3FD { background-position: -340px -520px; }
-.emoji-1F44E-1F3FE { background-position: -360px -520px; }
-.emoji-1F44E-1F3FF { background-position: -380px -520px; }
-.emoji-1F44F { background-position: -400px -520px; }
-.emoji-1F44F-1F3FB { background-position: -420px -520px; }
-.emoji-1F44F-1F3FC { background-position: -440px -520px; }
-.emoji-1F44F-1F3FD { background-position: -460px -520px; }
-.emoji-1F44F-1F3FE { background-position: -480px -520px; }
-.emoji-1F44F-1F3FF { background-position: -500px -520px; }
-.emoji-1F450 { background-position: -520px -520px; }
-.emoji-1F450-1F3FB { background-position: -540px 0; }
-.emoji-1F450-1F3FC { background-position: -540px -20px; }
-.emoji-1F450-1F3FD { background-position: -540px -40px; }
-.emoji-1F450-1F3FE { background-position: -540px -60px; }
-.emoji-1F450-1F3FF { background-position: -540px -80px; }
-.emoji-1F451 { background-position: -540px -100px; }
-.emoji-1F452 { background-position: -540px -120px; }
-.emoji-1F453 { background-position: -540px -140px; }
-.emoji-1F454 { background-position: -540px -160px; }
-.emoji-1F455 { background-position: -540px -180px; }
-.emoji-1F456 { background-position: -540px -200px; }
-.emoji-1F457 { background-position: -540px -220px; }
-.emoji-1F458 { background-position: -540px -240px; }
-.emoji-1F459 { background-position: -540px -260px; }
-.emoji-1F45A { background-position: -540px -280px; }
-.emoji-1F45B { background-position: -540px -300px; }
-.emoji-1F45C { background-position: -540px -320px; }
-.emoji-1F45D { background-position: -540px -340px; }
-.emoji-1F45E { background-position: -540px -360px; }
-.emoji-1F45F { background-position: -540px -380px; }
-.emoji-1F460 { background-position: -540px -400px; }
-.emoji-1F461 { background-position: -540px -420px; }
-.emoji-1F462 { background-position: -540px -440px; }
-.emoji-1F463 { background-position: -540px -460px; }
-.emoji-1F464 { background-position: -540px -480px; }
-.emoji-1F465 { background-position: -540px -500px; }
-.emoji-1F466 { background-position: -540px -520px; }
-.emoji-1F466-1F3FB { background-position: 0 -540px; }
-.emoji-1F466-1F3FC { background-position: -20px -540px; }
-.emoji-1F466-1F3FD { background-position: -40px -540px; }
-.emoji-1F466-1F3FE { background-position: -60px -540px; }
-.emoji-1F466-1F3FF { background-position: -80px -540px; }
-.emoji-1F467 { background-position: -100px -540px; }
-.emoji-1F467-1F3FB { background-position: -120px -540px; }
-.emoji-1F467-1F3FC { background-position: -140px -540px; }
-.emoji-1F467-1F3FD { background-position: -160px -540px; }
-.emoji-1F467-1F3FE { background-position: -180px -540px; }
-.emoji-1F467-1F3FF { background-position: -200px -540px; }
-.emoji-1F468 { background-position: -220px -540px; }
-.emoji-1F468-1F3FB { background-position: -240px -540px; }
-.emoji-1F468-1F3FC { background-position: -260px -540px; }
-.emoji-1F468-1F3FD { background-position: -280px -540px; }
-.emoji-1F468-1F3FE { background-position: -300px -540px; }
-.emoji-1F468-1F3FF { background-position: -320px -540px; }
-.emoji-1F468-1F468-1F466 { background-position: -340px -540px; }
-.emoji-1F468-1F468-1F466-1F466 { background-position: -360px -540px; }
-.emoji-1F468-1F468-1F467 { background-position: -380px -540px; }
-.emoji-1F468-1F468-1F467-1F466 { background-position: -400px -540px; }
-.emoji-1F468-1F468-1F467-1F467 { background-position: -420px -540px; }
-.emoji-1F468-1F469-1F466-1F466 { background-position: -440px -540px; }
-.emoji-1F468-1F469-1F467 { background-position: -460px -540px; }
-.emoji-1F468-1F469-1F467-1F466 { background-position: -480px -540px; }
-.emoji-1F468-1F469-1F467-1F467 { background-position: -500px -540px; }
-.emoji-1F468-2764-1F468 { background-position: -520px -540px; }
-.emoji-1F468-2764-1F48B-1F468 { background-position: -540px -540px; }
-.emoji-1F469 { background-position: -560px 0; }
-.emoji-1F469-1F3FB { background-position: -560px -20px; }
-.emoji-1F469-1F3FC { background-position: -560px -40px; }
-.emoji-1F469-1F3FD { background-position: -560px -60px; }
-.emoji-1F469-1F3FE { background-position: -560px -80px; }
-.emoji-1F469-1F3FF { background-position: -560px -100px; }
-.emoji-1F469-1F469-1F466 { background-position: -560px -120px; }
-.emoji-1F469-1F469-1F466-1F466 { background-position: -560px -140px; }
-.emoji-1F469-1F469-1F467 { background-position: -560px -160px; }
-.emoji-1F469-1F469-1F467-1F466 { background-position: -560px -180px; }
-.emoji-1F469-1F469-1F467-1F467 { background-position: -560px -200px; }
-.emoji-1F469-2764-1F469 { background-position: -560px -220px; }
-.emoji-1F469-2764-1F48B-1F469 { background-position: -560px -240px; }
-.emoji-1F46A { background-position: -560px -260px; }
-.emoji-1F46B { background-position: -560px -280px; }
-.emoji-1F46C { background-position: -560px -300px; }
-.emoji-1F46D { background-position: -560px -320px; }
-.emoji-1F46E { background-position: -560px -340px; }
-.emoji-1F46E-1F3FB { background-position: -560px -360px; }
-.emoji-1F46E-1F3FC { background-position: -560px -380px; }
-.emoji-1F46E-1F3FD { background-position: -560px -400px; }
-.emoji-1F46E-1F3FE { background-position: -560px -420px; }
-.emoji-1F46E-1F3FF { background-position: -560px -440px; }
-.emoji-1F46F { background-position: -560px -460px; }
-.emoji-1F470 { background-position: -560px -480px; }
-.emoji-1F470-1F3FB { background-position: -560px -500px; }
-.emoji-1F470-1F3FC { background-position: -560px -520px; }
-.emoji-1F470-1F3FD { background-position: -560px -540px; }
-.emoji-1F470-1F3FE { background-position: 0 -560px; }
-.emoji-1F470-1F3FF { background-position: -20px -560px; }
-.emoji-1F471 { background-position: -40px -560px; }
-.emoji-1F471-1F3FB { background-position: -60px -560px; }
-.emoji-1F471-1F3FC { background-position: -80px -560px; }
-.emoji-1F471-1F3FD { background-position: -100px -560px; }
-.emoji-1F471-1F3FE { background-position: -120px -560px; }
-.emoji-1F471-1F3FF { background-position: -140px -560px; }
-.emoji-1F472 { background-position: -160px -560px; }
-.emoji-1F472-1F3FB { background-position: -180px -560px; }
-.emoji-1F472-1F3FC { background-position: -200px -560px; }
-.emoji-1F472-1F3FD { background-position: -220px -560px; }
-.emoji-1F472-1F3FE { background-position: -240px -560px; }
-.emoji-1F472-1F3FF { background-position: -260px -560px; }
-.emoji-1F473 { background-position: -280px -560px; }
-.emoji-1F473-1F3FB { background-position: -300px -560px; }
-.emoji-1F473-1F3FC { background-position: -320px -560px; }
-.emoji-1F473-1F3FD { background-position: -340px -560px; }
-.emoji-1F473-1F3FE { background-position: -360px -560px; }
-.emoji-1F473-1F3FF { background-position: -380px -560px; }
-.emoji-1F474 { background-position: -400px -560px; }
-.emoji-1F474-1F3FB { background-position: -420px -560px; }
-.emoji-1F474-1F3FC { background-position: -440px -560px; }
-.emoji-1F474-1F3FD { background-position: -460px -560px; }
-.emoji-1F474-1F3FE { background-position: -480px -560px; }
-.emoji-1F474-1F3FF { background-position: -500px -560px; }
-.emoji-1F475 { background-position: -520px -560px; }
-.emoji-1F475-1F3FB { background-position: -540px -560px; }
-.emoji-1F475-1F3FC { background-position: -560px -560px; }
-.emoji-1F475-1F3FD { background-position: -580px 0; }
-.emoji-1F475-1F3FE { background-position: -580px -20px; }
-.emoji-1F475-1F3FF { background-position: -580px -40px; }
-.emoji-1F476 { background-position: -580px -60px; }
-.emoji-1F476-1F3FB { background-position: -580px -80px; }
-.emoji-1F476-1F3FC { background-position: -580px -100px; }
-.emoji-1F476-1F3FD { background-position: -580px -120px; }
-.emoji-1F476-1F3FE { background-position: -580px -140px; }
-.emoji-1F476-1F3FF { background-position: -580px -160px; }
-.emoji-1F477 { background-position: -580px -180px; }
-.emoji-1F477-1F3FB { background-position: -580px -200px; }
-.emoji-1F477-1F3FC { background-position: -580px -220px; }
-.emoji-1F477-1F3FD { background-position: -580px -240px; }
-.emoji-1F477-1F3FE { background-position: -580px -260px; }
-.emoji-1F477-1F3FF { background-position: -580px -280px; }
-.emoji-1F478 { background-position: -580px -300px; }
-.emoji-1F478-1F3FB { background-position: -580px -320px; }
-.emoji-1F478-1F3FC { background-position: -580px -340px; }
-.emoji-1F478-1F3FD { background-position: -580px -360px; }
-.emoji-1F478-1F3FE { background-position: -580px -380px; }
-.emoji-1F478-1F3FF { background-position: -580px -400px; }
-.emoji-1F479 { background-position: -580px -420px; }
-.emoji-1F47A { background-position: -580px -440px; }
-.emoji-1F47B { background-position: -580px -460px; }
-.emoji-1F47C { background-position: -580px -480px; }
-.emoji-1F47C-1F3FB { background-position: -580px -500px; }
-.emoji-1F47C-1F3FC { background-position: -580px -520px; }
-.emoji-1F47C-1F3FD { background-position: -580px -540px; }
-.emoji-1F47C-1F3FE { background-position: -580px -560px; }
-.emoji-1F47C-1F3FF { background-position: 0 -580px; }
-.emoji-1F47D { background-position: -20px -580px; }
-.emoji-1F47E { background-position: -40px -580px; }
-.emoji-1F47F { background-position: -60px -580px; }
-.emoji-1F480 { background-position: -80px -580px; }
-.emoji-1F481 { background-position: -100px -580px; }
-.emoji-1F481-1F3FB { background-position: -120px -580px; }
-.emoji-1F481-1F3FC { background-position: -140px -580px; }
-.emoji-1F481-1F3FD { background-position: -160px -580px; }
-.emoji-1F481-1F3FE { background-position: -180px -580px; }
-.emoji-1F481-1F3FF { background-position: -200px -580px; }
-.emoji-1F482 { background-position: -220px -580px; }
-.emoji-1F482-1F3FB { background-position: -240px -580px; }
-.emoji-1F482-1F3FC { background-position: -260px -580px; }
-.emoji-1F482-1F3FD { background-position: -280px -580px; }
-.emoji-1F482-1F3FE { background-position: -300px -580px; }
-.emoji-1F482-1F3FF { background-position: -320px -580px; }
-.emoji-1F483 { background-position: -340px -580px; }
-.emoji-1F483-1F3FB { background-position: -360px -580px; }
-.emoji-1F483-1F3FC { background-position: -380px -580px; }
-.emoji-1F483-1F3FD { background-position: -400px -580px; }
-.emoji-1F483-1F3FE { background-position: -420px -580px; }
-.emoji-1F483-1F3FF { background-position: -440px -580px; }
-.emoji-1F484 { background-position: -460px -580px; }
-.emoji-1F485 { background-position: -480px -580px; }
-.emoji-1F485-1F3FB { background-position: -500px -580px; }
-.emoji-1F485-1F3FC { background-position: -520px -580px; }
-.emoji-1F485-1F3FD { background-position: -540px -580px; }
-.emoji-1F485-1F3FE { background-position: -560px -580px; }
-.emoji-1F485-1F3FF { background-position: -580px -580px; }
-.emoji-1F486 { background-position: -600px 0; }
-.emoji-1F486-1F3FB { background-position: -600px -20px; }
-.emoji-1F486-1F3FC { background-position: -600px -40px; }
-.emoji-1F486-1F3FD { background-position: -600px -60px; }
-.emoji-1F486-1F3FE { background-position: -600px -80px; }
-.emoji-1F486-1F3FF { background-position: -600px -100px; }
-.emoji-1F487 { background-position: -600px -120px; }
-.emoji-1F487-1F3FB { background-position: -600px -140px; }
-.emoji-1F487-1F3FC { background-position: -600px -160px; }
-.emoji-1F487-1F3FD { background-position: -600px -180px; }
-.emoji-1F487-1F3FE { background-position: -600px -200px; }
-.emoji-1F487-1F3FF { background-position: -600px -220px; }
-.emoji-1F488 { background-position: -600px -240px; }
-.emoji-1F489 { background-position: -600px -260px; }
-.emoji-1F48A { background-position: -600px -280px; }
-.emoji-1F48B { background-position: -600px -300px; }
-.emoji-1F48C { background-position: -600px -320px; }
-.emoji-1F48D { background-position: -600px -340px; }
-.emoji-1F48E { background-position: -600px -360px; }
-.emoji-1F48F { background-position: -600px -380px; }
-.emoji-1F490 { background-position: -600px -400px; }
-.emoji-1F491 { background-position: -600px -420px; }
-.emoji-1F492 { background-position: -600px -440px; }
-.emoji-1F493 { background-position: -600px -460px; }
-.emoji-1F494 { background-position: -600px -480px; }
-.emoji-1F495 { background-position: -600px -500px; }
-.emoji-1F496 { background-position: -600px -520px; }
-.emoji-1F497 { background-position: -600px -540px; }
-.emoji-1F498 { background-position: -600px -560px; }
-.emoji-1F499 { background-position: -600px -580px; }
-.emoji-1F49A { background-position: 0 -600px; }
-.emoji-1F49B { background-position: -20px -600px; }
-.emoji-1F49C { background-position: -40px -600px; }
-.emoji-1F49D { background-position: -60px -600px; }
-.emoji-1F49E { background-position: -80px -600px; }
-.emoji-1F49F { background-position: -100px -600px; }
-.emoji-1F4A0 { background-position: -120px -600px; }
-.emoji-1F4A1 { background-position: -140px -600px; }
-.emoji-1F4A2 { background-position: -160px -600px; }
-.emoji-1F4A3 { background-position: -180px -600px; }
-.emoji-1F4A4 { background-position: -200px -600px; }
-.emoji-1F4A5 { background-position: -220px -600px; }
-.emoji-1F4A6 { background-position: -240px -600px; }
-.emoji-1F4A7 { background-position: -260px -600px; }
-.emoji-1F4A8 { background-position: -280px -600px; }
-.emoji-1F4A9 { background-position: -300px -600px; }
-.emoji-1F4AA { background-position: -320px -600px; }
-.emoji-1F4AA-1F3FB { background-position: -340px -600px; }
-.emoji-1F4AA-1F3FC { background-position: -360px -600px; }
-.emoji-1F4AA-1F3FD { background-position: -380px -600px; }
-.emoji-1F4AA-1F3FE { background-position: -400px -600px; }
-.emoji-1F4AA-1F3FF { background-position: -420px -600px; }
-.emoji-1F4AB { background-position: -440px -600px; }
-.emoji-1F4AC { background-position: -460px -600px; }
-.emoji-1F4AD { background-position: -480px -600px; }
-.emoji-1F4AE { background-position: -500px -600px; }
-.emoji-1F4AF { background-position: -520px -600px; }
-.emoji-1F4B0 { background-position: -540px -600px; }
-.emoji-1F4B1 { background-position: -560px -600px; }
-.emoji-1F4B2 { background-position: -580px -600px; }
-.emoji-1F4B3 { background-position: -600px -600px; }
-.emoji-1F4B4 { background-position: -620px 0; }
-.emoji-1F4B5 { background-position: -620px -20px; }
-.emoji-1F4B6 { background-position: -620px -40px; }
-.emoji-1F4B7 { background-position: -620px -60px; }
-.emoji-1F4B8 { background-position: -620px -80px; }
-.emoji-1F4B9 { background-position: -620px -100px; }
-.emoji-1F4BA { background-position: -620px -120px; }
-.emoji-1F4BB { background-position: -620px -140px; }
-.emoji-1F4BC { background-position: -620px -160px; }
-.emoji-1F4BD { background-position: -620px -180px; }
-.emoji-1F4BE { background-position: -620px -200px; }
-.emoji-1F4BF { background-position: -620px -220px; }
-.emoji-1F4C0 { background-position: -620px -240px; }
-.emoji-1F4C1 { background-position: -620px -260px; }
-.emoji-1F4C2 { background-position: -620px -280px; }
-.emoji-1F4C3 { background-position: -620px -300px; }
-.emoji-1F4C4 { background-position: -620px -320px; }
-.emoji-1F4C5 { background-position: -620px -340px; }
-.emoji-1F4C6 { background-position: -620px -360px; }
-.emoji-1F4C7 { background-position: -620px -380px; }
-.emoji-1F4C8 { background-position: -620px -400px; }
-.emoji-1F4C9 { background-position: -620px -420px; }
-.emoji-1F4CA { background-position: -620px -440px; }
-.emoji-1F4CB { background-position: -620px -460px; }
-.emoji-1F4CC { background-position: -620px -480px; }
-.emoji-1F4CD { background-position: -620px -500px; }
-.emoji-1F4CE { background-position: -620px -520px; }
-.emoji-1F4CF { background-position: -620px -540px; }
-.emoji-1F4D0 { background-position: -620px -560px; }
-.emoji-1F4D1 { background-position: -620px -580px; }
-.emoji-1F4D2 { background-position: -620px -600px; }
-.emoji-1F4D3 { background-position: 0 -620px; }
-.emoji-1F4D4 { background-position: -20px -620px; }
-.emoji-1F4D5 { background-position: -40px -620px; }
-.emoji-1F4D6 { background-position: -60px -620px; }
-.emoji-1F4D7 { background-position: -80px -620px; }
-.emoji-1F4D8 { background-position: -100px -620px; }
-.emoji-1F4D9 { background-position: -120px -620px; }
-.emoji-1F4DA { background-position: -140px -620px; }
-.emoji-1F4DB { background-position: -160px -620px; }
-.emoji-1F4DC { background-position: -180px -620px; }
-.emoji-1F4DD { background-position: -200px -620px; }
-.emoji-1F4DE { background-position: -220px -620px; }
-.emoji-1F4DF { background-position: -240px -620px; }
-.emoji-1F4E0 { background-position: -260px -620px; }
-.emoji-1F4E1 { background-position: -280px -620px; }
-.emoji-1F4E2 { background-position: -300px -620px; }
-.emoji-1F4E3 { background-position: -320px -620px; }
-.emoji-1F4E4 { background-position: -340px -620px; }
-.emoji-1F4E5 { background-position: -360px -620px; }
-.emoji-1F4E6 { background-position: -380px -620px; }
-.emoji-1F4E7 { background-position: -400px -620px; }
-.emoji-1F4E8 { background-position: -420px -620px; }
-.emoji-1F4E9 { background-position: -440px -620px; }
-.emoji-1F4EA { background-position: -460px -620px; }
-.emoji-1F4EB { background-position: -480px -620px; }
-.emoji-1F4EC { background-position: -500px -620px; }
-.emoji-1F4ED { background-position: -520px -620px; }
-.emoji-1F4EE { background-position: -540px -620px; }
-.emoji-1F4EF { background-position: -560px -620px; }
-.emoji-1F4F0 { background-position: -580px -620px; }
-.emoji-1F4F1 { background-position: -600px -620px; }
-.emoji-1F4F2 { background-position: -620px -620px; }
-.emoji-1F4F3 { background-position: -640px 0; }
-.emoji-1F4F4 { background-position: -640px -20px; }
-.emoji-1F4F5 { background-position: -640px -40px; }
-.emoji-1F4F6 { background-position: -640px -60px; }
-.emoji-1F4F7 { background-position: -640px -80px; }
-.emoji-1F4F8 { background-position: -640px -100px; }
-.emoji-1F4F9 { background-position: -640px -120px; }
-.emoji-1F4FA { background-position: -640px -140px; }
-.emoji-1F4FB { background-position: -640px -160px; }
-.emoji-1F4FC { background-position: -640px -180px; }
-.emoji-1F4FD { background-position: -640px -200px; }
-.emoji-1F4FE { background-position: -640px -220px; }
-.emoji-1F4FF { background-position: -640px -240px; }
-.emoji-1F500 { background-position: -640px -260px; }
-.emoji-1F501 { background-position: -640px -280px; }
-.emoji-1F502 { background-position: -640px -300px; }
-.emoji-1F503 { background-position: -640px -320px; }
-.emoji-1F504 { background-position: -640px -340px; }
-.emoji-1F505 { background-position: -640px -360px; }
-.emoji-1F506 { background-position: -640px -380px; }
-.emoji-1F507 { background-position: -640px -400px; }
-.emoji-1F508 { background-position: -640px -420px; }
-.emoji-1F509 { background-position: -640px -440px; }
-.emoji-1F50A { background-position: -640px -460px; }
-.emoji-1F50B { background-position: -640px -480px; }
-.emoji-1F50C { background-position: -640px -500px; }
-.emoji-1F50D { background-position: -640px -520px; }
-.emoji-1F50E { background-position: -640px -540px; }
-.emoji-1F50F { background-position: -640px -560px; }
-.emoji-1F510 { background-position: -640px -580px; }
-.emoji-1F511 { background-position: -640px -600px; }
-.emoji-1F512 { background-position: -640px -620px; }
-.emoji-1F513 { background-position: 0 -640px; }
-.emoji-1F514 { background-position: -20px -640px; }
-.emoji-1F515 { background-position: -40px -640px; }
-.emoji-1F516 { background-position: -60px -640px; }
-.emoji-1F517 { background-position: -80px -640px; }
-.emoji-1F518 { background-position: -100px -640px; }
-.emoji-1F519 { background-position: -120px -640px; }
-.emoji-1F51A { background-position: -140px -640px; }
-.emoji-1F51B { background-position: -160px -640px; }
-.emoji-1F51C { background-position: -180px -640px; }
-.emoji-1F51D { background-position: -200px -640px; }
-.emoji-1F51E { background-position: -220px -640px; }
-.emoji-1F51F { background-position: -240px -640px; }
-.emoji-1F520 { background-position: -260px -640px; }
-.emoji-1F521 { background-position: -280px -640px; }
-.emoji-1F522 { background-position: -300px -640px; }
-.emoji-1F523 { background-position: -320px -640px; }
-.emoji-1F524 { background-position: -340px -640px; }
-.emoji-1F525 { background-position: -360px -640px; }
-.emoji-1F526 { background-position: -380px -640px; }
-.emoji-1F527 { background-position: -400px -640px; }
-.emoji-1F528 { background-position: -420px -640px; }
-.emoji-1F529 { background-position: -440px -640px; }
-.emoji-1F52A { background-position: -460px -640px; }
-.emoji-1F52B { background-position: -480px -640px; }
-.emoji-1F52C { background-position: -500px -640px; }
-.emoji-1F52D { background-position: -520px -640px; }
-.emoji-1F52E { background-position: -540px -640px; }
-.emoji-1F52F { background-position: -560px -640px; }
-.emoji-1F530 { background-position: -580px -640px; }
-.emoji-1F531 { background-position: -600px -640px; }
-.emoji-1F532 { background-position: -620px -640px; }
-.emoji-1F533 { background-position: -640px -640px; }
-.emoji-1F534 { background-position: -660px 0; }
-.emoji-1F535 { background-position: -660px -20px; }
-.emoji-1F536 { background-position: -660px -40px; }
-.emoji-1F537 { background-position: -660px -60px; }
-.emoji-1F538 { background-position: -660px -80px; }
-.emoji-1F539 { background-position: -660px -100px; }
-.emoji-1F53A { background-position: -660px -120px; }
-.emoji-1F53B { background-position: -660px -140px; }
-.emoji-1F53C { background-position: -660px -160px; }
-.emoji-1F53D { background-position: -660px -180px; }
-.emoji-1F546 { background-position: -660px -200px; }
-.emoji-1F547 { background-position: -660px -220px; }
-.emoji-1F548 { background-position: -660px -240px; }
-.emoji-1F549 { background-position: -660px -260px; }
-.emoji-1F54A { background-position: -660px -280px; }
-.emoji-1F54B { background-position: -660px -300px; }
-.emoji-1F54C { background-position: -660px -320px; }
-.emoji-1F54D { background-position: -660px -340px; }
-.emoji-1F54E { background-position: -660px -360px; }
-.emoji-1F550 { background-position: -660px -380px; }
-.emoji-1F551 { background-position: -660px -400px; }
-.emoji-1F552 { background-position: -660px -420px; }
-.emoji-1F553 { background-position: -660px -440px; }
-.emoji-1F554 { background-position: -660px -460px; }
-.emoji-1F555 { background-position: -660px -480px; }
-.emoji-1F556 { background-position: -660px -500px; }
-.emoji-1F557 { background-position: -660px -520px; }
-.emoji-1F558 { background-position: -660px -540px; }
-.emoji-1F559 { background-position: -660px -560px; }
-.emoji-1F55A { background-position: -660px -580px; }
-.emoji-1F55B { background-position: -660px -600px; }
-.emoji-1F55C { background-position: -660px -620px; }
-.emoji-1F55D { background-position: -660px -640px; }
-.emoji-1F55E { background-position: 0 -660px; }
-.emoji-1F55F { background-position: -20px -660px; }
-.emoji-1F560 { background-position: -40px -660px; }
-.emoji-1F561 { background-position: -60px -660px; }
-.emoji-1F562 { background-position: -80px -660px; }
-.emoji-1F563 { background-position: -100px -660px; }
-.emoji-1F564 { background-position: -120px -660px; }
-.emoji-1F565 { background-position: -140px -660px; }
-.emoji-1F566 { background-position: -160px -660px; }
-.emoji-1F567 { background-position: -180px -660px; }
-.emoji-1F568 { background-position: -200px -660px; }
-.emoji-1F569 { background-position: -220px -660px; }
-.emoji-1F56A { background-position: -240px -660px; }
-.emoji-1F56B { background-position: -260px -660px; }
-.emoji-1F56C { background-position: -280px -660px; }
-.emoji-1F56D { background-position: -300px -660px; }
-.emoji-1F56E { background-position: -320px -660px; }
-.emoji-1F56F { background-position: -340px -660px; }
-.emoji-1F570 { background-position: -360px -660px; }
-.emoji-1F571 { background-position: -380px -660px; }
-.emoji-1F572 { background-position: -400px -660px; }
-.emoji-1F573 { background-position: -420px -660px; }
-.emoji-1F574 { background-position: -440px -660px; }
-.emoji-1F575 { background-position: -460px -660px; }
-.emoji-1F575-1F3FB { background-position: -480px -660px; }
-.emoji-1F575-1F3FC { background-position: -500px -660px; }
-.emoji-1F575-1F3FD { background-position: -520px -660px; }
-.emoji-1F575-1F3FE { background-position: -540px -660px; }
-.emoji-1F575-1F3FF { background-position: -560px -660px; }
-.emoji-1F576 { background-position: -580px -660px; }
-.emoji-1F577 { background-position: -600px -660px; }
-.emoji-1F578 { background-position: -620px -660px; }
-.emoji-1F579 { background-position: -640px -660px; }
-.emoji-1F57B { background-position: -660px -660px; }
-.emoji-1F57E { background-position: -680px 0; }
-.emoji-1F57F { background-position: -680px -20px; }
-.emoji-1F581 { background-position: -680px -40px; }
-.emoji-1F582 { background-position: -680px -60px; }
-.emoji-1F583 { background-position: -680px -80px; }
-.emoji-1F585 { background-position: -680px -100px; }
-.emoji-1F586 { background-position: -680px -120px; }
-.emoji-1F587 { background-position: -680px -140px; }
-.emoji-1F588 { background-position: -680px -160px; }
-.emoji-1F589 { background-position: -680px -180px; }
-.emoji-1F58A { background-position: -680px -200px; }
-.emoji-1F58B { background-position: -680px -220px; }
-.emoji-1F58C { background-position: -680px -240px; }
-.emoji-1F58D { background-position: -680px -260px; }
-.emoji-1F58E { background-position: -680px -280px; }
-.emoji-1F58F { background-position: -680px -300px; }
-.emoji-1F590 { background-position: -680px -320px; }
-.emoji-1F590-1F3FB { background-position: -680px -340px; }
-.emoji-1F590-1F3FC { background-position: -680px -360px; }
-.emoji-1F590-1F3FD { background-position: -680px -380px; }
-.emoji-1F590-1F3FE { background-position: -680px -400px; }
-.emoji-1F590-1F3FF { background-position: -680px -420px; }
-.emoji-1F591 { background-position: -680px -440px; }
-.emoji-1F592 { background-position: -680px -460px; }
-.emoji-1F593 { background-position: -680px -480px; }
-.emoji-1F594 { background-position: -680px -500px; }
-.emoji-1F595 { background-position: -680px -520px; }
-.emoji-1F595-1F3FB { background-position: -680px -540px; }
-.emoji-1F595-1F3FC { background-position: -680px -560px; }
-.emoji-1F595-1F3FD { background-position: -680px -580px; }
-.emoji-1F595-1F3FE { background-position: -680px -600px; }
-.emoji-1F595-1F3FF { background-position: -680px -620px; }
-.emoji-1F596 { background-position: -680px -640px; }
-.emoji-1F596-1F3FB { background-position: -680px -660px; }
-.emoji-1F596-1F3FC { background-position: 0 -680px; }
-.emoji-1F596-1F3FD { background-position: -20px -680px; }
-.emoji-1F596-1F3FE { background-position: -40px -680px; }
-.emoji-1F596-1F3FF { background-position: -60px -680px; }
-.emoji-1F597 { background-position: -80px -680px; }
-.emoji-1F598 { background-position: -100px -680px; }
-.emoji-1F599 { background-position: -120px -680px; }
-.emoji-1F59E { background-position: -140px -680px; }
-.emoji-1F59F { background-position: -160px -680px; }
-.emoji-1F5A5 { background-position: -180px -680px; }
-.emoji-1F5A6 { background-position: -200px -680px; }
-.emoji-1F5A7 { background-position: -220px -680px; }
-.emoji-1F5A8 { background-position: -240px -680px; }
-.emoji-1F5A9 { background-position: -260px -680px; }
-.emoji-1F5AA { background-position: -280px -680px; }
-.emoji-1F5AB { background-position: -300px -680px; }
-.emoji-1F5AD { background-position: -320px -680px; }
-.emoji-1F5AE { background-position: -340px -680px; }
-.emoji-1F5AF { background-position: -360px -680px; }
-.emoji-1F5B1 { background-position: -380px -680px; }
-.emoji-1F5B2 { background-position: -400px -680px; }
-.emoji-1F5B3 { background-position: -420px -680px; }
-.emoji-1F5B4 { background-position: -440px -680px; }
-.emoji-1F5B8 { background-position: -460px -680px; }
-.emoji-1F5B9 { background-position: -480px -680px; }
-.emoji-1F5BC { background-position: -500px -680px; }
-.emoji-1F5BD { background-position: -520px -680px; }
-.emoji-1F5BE { background-position: -540px -680px; }
-.emoji-1F5C0 { background-position: -560px -680px; }
-.emoji-1F5C1 { background-position: -580px -680px; }
-.emoji-1F5C2 { background-position: -600px -680px; }
-.emoji-1F5C3 { background-position: -620px -680px; }
-.emoji-1F5C4 { background-position: -640px -680px; }
-.emoji-1F5C6 { background-position: -660px -680px; }
-.emoji-1F5C7 { background-position: -680px -680px; }
-.emoji-1F5C9 { background-position: -700px 0; }
-.emoji-1F5CA { background-position: -700px -20px; }
-.emoji-1F5CE { background-position: -700px -40px; }
-.emoji-1F5CF { background-position: -700px -60px; }
-.emoji-1F5D0 { background-position: -700px -80px; }
-.emoji-1F5D1 { background-position: -700px -100px; }
-.emoji-1F5D2 { background-position: -700px -120px; }
-.emoji-1F5D3 { background-position: -700px -140px; }
-.emoji-1F5D4 { background-position: -700px -160px; }
-.emoji-1F5D8 { background-position: -700px -180px; }
-.emoji-1F5D9 { background-position: -700px -200px; }
-.emoji-1F5DC { background-position: -700px -220px; }
-.emoji-1F5DD { background-position: -700px -240px; }
-.emoji-1F5DE { background-position: -700px -260px; }
-.emoji-1F5E0 { background-position: -700px -280px; }
-.emoji-1F5E1 { background-position: -700px -300px; }
-.emoji-1F5E2 { background-position: -700px -320px; }
-.emoji-1F5E3 { background-position: -700px -340px; }
-.emoji-1F5E8 { background-position: -700px -360px; }
-.emoji-1F5E9 { background-position: -700px -380px; }
-.emoji-1F5EA { background-position: -700px -400px; }
-.emoji-1F5EB { background-position: -700px -420px; }
-.emoji-1F5EC { background-position: -700px -440px; }
-.emoji-1F5ED { background-position: -700px -460px; }
-.emoji-1F5EE { background-position: -700px -480px; }
-.emoji-1F5EF { background-position: -700px -500px; }
-.emoji-1F5F0 { background-position: -700px -520px; }
-.emoji-1F5F1 { background-position: -700px -540px; }
-.emoji-1F5F2 { background-position: -700px -560px; }
-.emoji-1F5F3 { background-position: -700px -580px; }
-.emoji-1F5F4 { background-position: -700px -600px; }
-.emoji-1F5F5 { background-position: -700px -620px; }
-.emoji-1F5F8 { background-position: -700px -640px; }
-.emoji-1F5F9 { background-position: -700px -660px; }
-.emoji-1F5FA { background-position: -700px -680px; }
-.emoji-1F5FB { background-position: 0 -700px; }
-.emoji-1F5FC { background-position: -20px -700px; }
-.emoji-1F5FD { background-position: -40px -700px; }
-.emoji-1F5FE { background-position: -60px -700px; }
-.emoji-1F5FF { background-position: -80px -700px; }
-.emoji-1F600 { background-position: -100px -700px; }
-.emoji-1F601 { background-position: -120px -700px; }
-.emoji-1F602 { background-position: -140px -700px; }
-.emoji-1F603 { background-position: -160px -700px; }
-.emoji-1F604 { background-position: -180px -700px; }
-.emoji-1F605 { background-position: -200px -700px; }
-.emoji-1F606 { background-position: -220px -700px; }
-.emoji-1F607 { background-position: -240px -700px; }
-.emoji-1F608 { background-position: -260px -700px; }
-.emoji-1F609 { background-position: -280px -700px; }
-.emoji-1F60A { background-position: -300px -700px; }
-.emoji-1F60B { background-position: -320px -700px; }
-.emoji-1F60C { background-position: -340px -700px; }
-.emoji-1F60D { background-position: -360px -700px; }
-.emoji-1F60E { background-position: -380px -700px; }
-.emoji-1F60F { background-position: -400px -700px; }
-.emoji-1F610 { background-position: -420px -700px; }
-.emoji-1F611 { background-position: -440px -700px; }
-.emoji-1F612 { background-position: -460px -700px; }
-.emoji-1F613 { background-position: -480px -700px; }
-.emoji-1F614 { background-position: -500px -700px; }
-.emoji-1F615 { background-position: -520px -700px; }
-.emoji-1F616 { background-position: -540px -700px; }
-.emoji-1F617 { background-position: -560px -700px; }
-.emoji-1F618 { background-position: -580px -700px; }
-.emoji-1F619 { background-position: -600px -700px; }
-.emoji-1F61A { background-position: -620px -700px; }
-.emoji-1F61B { background-position: -640px -700px; }
-.emoji-1F61C { background-position: -660px -700px; }
-.emoji-1F61D { background-position: -680px -700px; }
-.emoji-1F61E { background-position: -700px -700px; }
-.emoji-1F61F { background-position: -720px 0; }
-.emoji-1F620 { background-position: -720px -20px; }
-.emoji-1F621 { background-position: -720px -40px; }
-.emoji-1F622 { background-position: -720px -60px; }
-.emoji-1F623 { background-position: -720px -80px; }
-.emoji-1F624 { background-position: -720px -100px; }
-.emoji-1F625 { background-position: -720px -120px; }
-.emoji-1F626 { background-position: -720px -140px; }
-.emoji-1F627 { background-position: -720px -160px; }
-.emoji-1F628 { background-position: -720px -180px; }
-.emoji-1F629 { background-position: -720px -200px; }
-.emoji-1F62A { background-position: -720px -220px; }
-.emoji-1F62B { background-position: -720px -240px; }
-.emoji-1F62C { background-position: -720px -260px; }
-.emoji-1F62D { background-position: -720px -280px; }
-.emoji-1F62E { background-position: -720px -300px; }
-.emoji-1F62F { background-position: -720px -320px; }
-.emoji-1F630 { background-position: -720px -340px; }
-.emoji-1F631 { background-position: -720px -360px; }
-.emoji-1F632 { background-position: -720px -380px; }
-.emoji-1F633 { background-position: -720px -400px; }
-.emoji-1F634 { background-position: -720px -420px; }
-.emoji-1F635 { background-position: -720px -440px; }
-.emoji-1F636 { background-position: -720px -460px; }
-.emoji-1F637 { background-position: -720px -480px; }
-.emoji-1F638 { background-position: -720px -500px; }
-.emoji-1F639 { background-position: -720px -520px; }
-.emoji-1F63A { background-position: -720px -540px; }
-.emoji-1F63B { background-position: -720px -560px; }
-.emoji-1F63C { background-position: -720px -580px; }
-.emoji-1F63D { background-position: -720px -600px; }
-.emoji-1F63E { background-position: -720px -620px; }
-.emoji-1F63F { background-position: -720px -640px; }
-.emoji-1F640 { background-position: -720px -660px; }
-.emoji-1F641 { background-position: -720px -680px; }
-.emoji-1F642 { background-position: -720px -700px; }
-.emoji-1F643 { background-position: 0 -720px; }
-.emoji-1F644 { background-position: -20px -720px; }
-.emoji-1F645 { background-position: -40px -720px; }
-.emoji-1F645-1F3FB { background-position: -60px -720px; }
-.emoji-1F645-1F3FC { background-position: -80px -720px; }
-.emoji-1F645-1F3FD { background-position: -100px -720px; }
-.emoji-1F645-1F3FE { background-position: -120px -720px; }
-.emoji-1F645-1F3FF { background-position: -140px -720px; }
-.emoji-1F646 { background-position: -160px -720px; }
-.emoji-1F646-1F3FB { background-position: -180px -720px; }
-.emoji-1F646-1F3FC { background-position: -200px -720px; }
-.emoji-1F646-1F3FD { background-position: -220px -720px; }
-.emoji-1F646-1F3FE { background-position: -240px -720px; }
-.emoji-1F646-1F3FF { background-position: -260px -720px; }
-.emoji-1F647 { background-position: -280px -720px; }
-.emoji-1F647-1F3FB { background-position: -300px -720px; }
-.emoji-1F647-1F3FC { background-position: -320px -720px; }
-.emoji-1F647-1F3FD { background-position: -340px -720px; }
-.emoji-1F647-1F3FE { background-position: -360px -720px; }
-.emoji-1F647-1F3FF { background-position: -380px -720px; }
-.emoji-1F648 { background-position: -400px -720px; }
-.emoji-1F649 { background-position: -420px -720px; }
-.emoji-1F64A { background-position: -440px -720px; }
-.emoji-1F64B { background-position: -460px -720px; }
-.emoji-1F64B-1F3FB { background-position: -480px -720px; }
-.emoji-1F64B-1F3FC { background-position: -500px -720px; }
-.emoji-1F64B-1F3FD { background-position: -520px -720px; }
-.emoji-1F64B-1F3FE { background-position: -540px -720px; }
-.emoji-1F64B-1F3FF { background-position: -560px -720px; }
-.emoji-1F64C { background-position: -580px -720px; }
-.emoji-1F64C-1F3FB { background-position: -600px -720px; }
-.emoji-1F64C-1F3FC { background-position: -620px -720px; }
-.emoji-1F64C-1F3FD { background-position: -640px -720px; }
-.emoji-1F64C-1F3FE { background-position: -660px -720px; }
-.emoji-1F64C-1F3FF { background-position: -680px -720px; }
-.emoji-1F64D { background-position: -700px -720px; }
-.emoji-1F64D-1F3FB { background-position: -720px -720px; }
-.emoji-1F64D-1F3FC { background-position: -740px 0; }
-.emoji-1F64D-1F3FD { background-position: -740px -20px; }
-.emoji-1F64D-1F3FE { background-position: -740px -40px; }
-.emoji-1F64D-1F3FF { background-position: -740px -60px; }
-.emoji-1F64E { background-position: -740px -80px; }
-.emoji-1F64E-1F3FB { background-position: -740px -100px; }
-.emoji-1F64E-1F3FC { background-position: -740px -120px; }
-.emoji-1F64E-1F3FD { background-position: -740px -140px; }
-.emoji-1F64E-1F3FE { background-position: -740px -160px; }
-.emoji-1F64E-1F3FF { background-position: -740px -180px; }
-.emoji-1F64F { background-position: -740px -200px; }
-.emoji-1F64F-1F3FB { background-position: -740px -220px; }
-.emoji-1F64F-1F3FC { background-position: -740px -240px; }
-.emoji-1F64F-1F3FD { background-position: -740px -260px; }
-.emoji-1F64F-1F3FE { background-position: -740px -280px; }
-.emoji-1F64F-1F3FF { background-position: -740px -300px; }
-.emoji-1F680 { background-position: -740px -320px; }
-.emoji-1F681 { background-position: -740px -340px; }
-.emoji-1F682 { background-position: -740px -360px; }
-.emoji-1F683 { background-position: -740px -380px; }
-.emoji-1F684 { background-position: -740px -400px; }
-.emoji-1F685 { background-position: -740px -420px; }
-.emoji-1F686 { background-position: -740px -440px; }
-.emoji-1F687 { background-position: -740px -460px; }
-.emoji-1F688 { background-position: -740px -480px; }
-.emoji-1F689 { background-position: -740px -500px; }
-.emoji-1F68A { background-position: -740px -520px; }
-.emoji-1F68B { background-position: -740px -540px; }
-.emoji-1F68C { background-position: -740px -560px; }
-.emoji-1F68D { background-position: -740px -580px; }
-.emoji-1F68E { background-position: -740px -600px; }
-.emoji-1F68F { background-position: -740px -620px; }
-.emoji-1F690 { background-position: -740px -640px; }
-.emoji-1F691 { background-position: -740px -660px; }
-.emoji-1F692 { background-position: -740px -680px; }
-.emoji-1F693 { background-position: -740px -700px; }
-.emoji-1F694 { background-position: -740px -720px; }
-.emoji-1F695 { background-position: 0 -740px; }
-.emoji-1F696 { background-position: -20px -740px; }
-.emoji-1F697 { background-position: -40px -740px; }
-.emoji-1F698 { background-position: -60px -740px; }
-.emoji-1F699 { background-position: -80px -740px; }
-.emoji-1F69A { background-position: -100px -740px; }
-.emoji-1F69B { background-position: -120px -740px; }
-.emoji-1F69C { background-position: -140px -740px; }
-.emoji-1F69D { background-position: -160px -740px; }
-.emoji-1F69E { background-position: -180px -740px; }
-.emoji-1F69F { background-position: -200px -740px; }
-.emoji-1F6A0 { background-position: -220px -740px; }
-.emoji-1F6A1 { background-position: -240px -740px; }
-.emoji-1F6A2 { background-position: -260px -740px; }
-.emoji-1F6A3 { background-position: -280px -740px; }
-.emoji-1F6A3-1F3FB { background-position: -300px -740px; }
-.emoji-1F6A3-1F3FC { background-position: -320px -740px; }
-.emoji-1F6A3-1F3FD { background-position: -340px -740px; }
-.emoji-1F6A3-1F3FE { background-position: -360px -740px; }
-.emoji-1F6A3-1F3FF { background-position: -380px -740px; }
-.emoji-1F6A4 { background-position: -400px -740px; }
-.emoji-1F6A5 { background-position: -420px -740px; }
-.emoji-1F6A6 { background-position: -440px -740px; }
-.emoji-1F6A7 { background-position: -460px -740px; }
-.emoji-1F6A8 { background-position: -480px -740px; }
-.emoji-1F6A9 { background-position: -500px -740px; }
-.emoji-1F6AA { background-position: -520px -740px; }
-.emoji-1F6AB { background-position: -540px -740px; }
-.emoji-1F6AC { background-position: -560px -740px; }
-.emoji-1F6AD { background-position: -580px -740px; }
-.emoji-1F6AE { background-position: -600px -740px; }
-.emoji-1F6AF { background-position: -620px -740px; }
-.emoji-1F6B0 { background-position: -640px -740px; }
-.emoji-1F6B1 { background-position: -660px -740px; }
-.emoji-1F6B2 { background-position: -680px -740px; }
-.emoji-1F6B3 { background-position: -700px -740px; }
-.emoji-1F6B4 { background-position: -720px -740px; }
-.emoji-1F6B4-1F3FB { background-position: -740px -740px; }
-.emoji-1F6B4-1F3FC { background-position: -760px 0; }
-.emoji-1F6B4-1F3FD { background-position: -760px -20px; }
-.emoji-1F6B4-1F3FE { background-position: -760px -40px; }
-.emoji-1F6B4-1F3FF { background-position: -760px -60px; }
-.emoji-1F6B5 { background-position: -760px -80px; }
-.emoji-1F6B5-1F3FB { background-position: -760px -100px; }
-.emoji-1F6B5-1F3FC { background-position: -760px -120px; }
-.emoji-1F6B5-1F3FD { background-position: -760px -140px; }
-.emoji-1F6B5-1F3FE { background-position: -760px -160px; }
-.emoji-1F6B5-1F3FF { background-position: -760px -180px; }
-.emoji-1F6B6 { background-position: -760px -200px; }
-.emoji-1F6B6-1F3FB { background-position: -760px -220px; }
-.emoji-1F6B6-1F3FC { background-position: -760px -240px; }
-.emoji-1F6B6-1F3FD { background-position: -760px -260px; }
-.emoji-1F6B6-1F3FE { background-position: -760px -280px; }
-.emoji-1F6B6-1F3FF { background-position: -760px -300px; }
-.emoji-1F6B7 { background-position: -760px -320px; }
-.emoji-1F6B8 { background-position: -760px -340px; }
-.emoji-1F6B9 { background-position: -760px -360px; }
-.emoji-1F6BA { background-position: -760px -380px; }
-.emoji-1F6BB { background-position: -760px -400px; }
-.emoji-1F6BC { background-position: -760px -420px; }
-.emoji-1F6BD { background-position: -760px -440px; }
-.emoji-1F6BE { background-position: -760px -460px; }
-.emoji-1F6BF { background-position: -760px -480px; }
-.emoji-1F6C0 { background-position: -760px -500px; }
-.emoji-1F6C0-1F3FB { background-position: -760px -520px; }
-.emoji-1F6C0-1F3FC { background-position: -760px -540px; }
-.emoji-1F6C0-1F3FD { background-position: -760px -560px; }
-.emoji-1F6C0-1F3FE { background-position: -760px -580px; }
-.emoji-1F6C0-1F3FF { background-position: -760px -600px; }
-.emoji-1F6C1 { background-position: -760px -620px; }
-.emoji-1F6C2 { background-position: -760px -640px; }
-.emoji-1F6C3 { background-position: -760px -660px; }
-.emoji-1F6C4 { background-position: -760px -680px; }
-.emoji-1F6C5 { background-position: -760px -700px; }
-.emoji-1F6C6 { background-position: -760px -720px; }
-.emoji-1F6C7 { background-position: -760px -740px; }
-.emoji-1F6C8 { background-position: 0 -760px; }
-.emoji-1F6C9 { background-position: -20px -760px; }
-.emoji-1F6CA { background-position: -40px -760px; }
-.emoji-1F6CB { background-position: -60px -760px; }
-.emoji-1F6CC { background-position: -80px -760px; }
-.emoji-1F6CD { background-position: -100px -760px; }
-.emoji-1F6CE { background-position: -120px -760px; }
-.emoji-1F6CF { background-position: -140px -760px; }
-.emoji-1F6D0 { background-position: -160px -760px; }
-.emoji-1F6E0 { background-position: -180px -760px; }
-.emoji-1F6E1 { background-position: -200px -760px; }
-.emoji-1F6E2 { background-position: -220px -760px; }
-.emoji-1F6E3 { background-position: -240px -760px; }
-.emoji-1F6E4 { background-position: -260px -760px; }
-.emoji-1F6E5 { background-position: -280px -760px; }
-.emoji-1F6E6 { background-position: -300px -760px; }
-.emoji-1F6E7 { background-position: -320px -760px; }
-.emoji-1F6E8 { background-position: -340px -760px; }
-.emoji-1F6E9 { background-position: -360px -760px; }
-.emoji-1F6EA { background-position: -380px -760px; }
-.emoji-1F6EB { background-position: -400px -760px; }
-.emoji-1F6EC { background-position: -420px -760px; }
-.emoji-1F6F0 { background-position: -440px -760px; }
-.emoji-1F6F1 { background-position: -460px -760px; }
-.emoji-1F6F2 { background-position: -480px -760px; }
-.emoji-1F6F3 { background-position: -500px -760px; }
-.emoji-1F910 { background-position: -520px -760px; }
-.emoji-1F911 { background-position: -540px -760px; }
-.emoji-1F912 { background-position: -560px -760px; }
-.emoji-1F913 { background-position: -580px -760px; }
-.emoji-1F914 { background-position: -600px -760px; }
-.emoji-1F915 { background-position: -620px -760px; }
-.emoji-1F916 { background-position: -640px -760px; }
-.emoji-1F917 { background-position: -660px -760px; }
-.emoji-1F918 { background-position: -680px -760px; }
-.emoji-1F918-1F3FB { background-position: -700px -760px; }
-.emoji-1F918-1F3FC { background-position: -720px -760px; }
-.emoji-1F918-1F3FD { background-position: -740px -760px; }
-.emoji-1F918-1F3FE { background-position: -760px -760px; }
-.emoji-1F918-1F3FF { background-position: -780px 0; }
-.emoji-1F980 { background-position: -780px -20px; }
-.emoji-1F981 { background-position: -780px -40px; }
-.emoji-1F982 { background-position: -780px -60px; }
-.emoji-1F983 { background-position: -780px -80px; }
-.emoji-1F984 { background-position: -780px -100px; }
-.emoji-1F9C0 { background-position: -780px -120px; }
-.emoji-203C { background-position: -780px -140px; }
-.emoji-2049 { background-position: -780px -160px; }
-.emoji-2122 { background-position: -780px -180px; }
-.emoji-2139 { background-position: -780px -200px; }
-.emoji-2194 { background-position: -780px -220px; }
-.emoji-2195 { background-position: -780px -240px; }
-.emoji-2196 { background-position: -780px -260px; }
-.emoji-2197 { background-position: -780px -280px; }
-.emoji-2198 { background-position: -780px -300px; }
-.emoji-2199 { background-position: -780px -320px; }
-.emoji-21A9 { background-position: -780px -340px; }
-.emoji-21AA { background-position: -780px -360px; }
-.emoji-231A { background-position: -780px -380px; }
-.emoji-231B { background-position: -780px -400px; }
-.emoji-2328 { background-position: -780px -420px; }
-.emoji-23E9 { background-position: -780px -440px; }
-.emoji-23EA { background-position: -780px -460px; }
-.emoji-23EB { background-position: -780px -480px; }
-.emoji-23EC { background-position: -780px -500px; }
-.emoji-23ED { background-position: -780px -520px; }
-.emoji-23EE { background-position: -780px -540px; }
-.emoji-23EF { background-position: -780px -560px; }
-.emoji-23F0 { background-position: -780px -580px; }
-.emoji-23F1 { background-position: -780px -600px; }
-.emoji-23F2 { background-position: -780px -620px; }
-.emoji-23F3 { background-position: -780px -640px; }
-.emoji-23F8 { background-position: -780px -660px; }
-.emoji-23F9 { background-position: -780px -680px; }
-.emoji-23FA { background-position: -780px -700px; }
-.emoji-24C2 { background-position: -780px -720px; }
-.emoji-25AA { background-position: -780px -740px; }
-.emoji-25AB { background-position: -780px -760px; }
-.emoji-25B6 { background-position: 0 -780px; }
-.emoji-25C0 { background-position: -20px -780px; }
-.emoji-25FB { background-position: -40px -780px; }
-.emoji-25FC { background-position: -60px -780px; }
-.emoji-25FD { background-position: -80px -780px; }
-.emoji-25FE { background-position: -100px -780px; }
-.emoji-2600 { background-position: -120px -780px; }
-.emoji-2601 { background-position: -140px -780px; }
-.emoji-2602 { background-position: -160px -780px; }
-.emoji-2603 { background-position: -180px -780px; }
-.emoji-2604 { background-position: -200px -780px; }
-.emoji-260E { background-position: -220px -780px; }
-.emoji-2611 { background-position: -240px -780px; }
-.emoji-2614 { background-position: -260px -780px; }
-.emoji-2615 { background-position: -280px -780px; }
-.emoji-2618 { background-position: -300px -780px; }
-.emoji-261D { background-position: -320px -780px; }
-.emoji-261D-1F3FB { background-position: -340px -780px; }
-.emoji-261D-1F3FC { background-position: -360px -780px; }
-.emoji-261D-1F3FD { background-position: -380px -780px; }
-.emoji-261D-1F3FE { background-position: -400px -780px; }
-.emoji-261D-1F3FF { background-position: -420px -780px; }
-.emoji-2620 { background-position: -440px -780px; }
-.emoji-2622 { background-position: -460px -780px; }
-.emoji-2623 { background-position: -480px -780px; }
-.emoji-2626 { background-position: -500px -780px; }
-.emoji-262A { background-position: -520px -780px; }
-.emoji-262E { background-position: -540px -780px; }
-.emoji-262F { background-position: -560px -780px; }
-.emoji-2638 { background-position: -580px -780px; }
-.emoji-2639 { background-position: -600px -780px; }
-.emoji-263A { background-position: -620px -780px; }
-.emoji-2648 { background-position: -640px -780px; }
-.emoji-2649 { background-position: -660px -780px; }
-.emoji-264A { background-position: -680px -780px; }
-.emoji-264B { background-position: -700px -780px; }
-.emoji-264C { background-position: -720px -780px; }
-.emoji-264D { background-position: -740px -780px; }
-.emoji-264E { background-position: -760px -780px; }
-.emoji-264F { background-position: -780px -780px; }
-.emoji-2650 { background-position: -800px 0; }
-.emoji-2651 { background-position: -800px -20px; }
-.emoji-2652 { background-position: -800px -40px; }
-.emoji-2653 { background-position: -800px -60px; }
-.emoji-2660 { background-position: -800px -80px; }
-.emoji-2663 { background-position: -800px -100px; }
-.emoji-2665 { background-position: -800px -120px; }
-.emoji-2666 { background-position: -800px -140px; }
-.emoji-2668 { background-position: -800px -160px; }
-.emoji-267B { background-position: -800px -180px; }
-.emoji-267F { background-position: -800px -200px; }
-.emoji-2692 { background-position: -800px -220px; }
-.emoji-2693 { background-position: -800px -240px; }
-.emoji-2694 { background-position: -800px -260px; }
-.emoji-2696 { background-position: -800px -280px; }
-.emoji-2697 { background-position: -800px -300px; }
-.emoji-2699 { background-position: -800px -320px; }
-.emoji-269B { background-position: -800px -340px; }
-.emoji-269C { background-position: -800px -360px; }
-.emoji-26A0 { background-position: -800px -380px; }
-.emoji-26A1 { background-position: -800px -400px; }
-.emoji-26AA { background-position: -800px -420px; }
-.emoji-26AB { background-position: -800px -440px; }
-.emoji-26B0 { background-position: -800px -460px; }
-.emoji-26B1 { background-position: -800px -480px; }
-.emoji-26BD { background-position: -800px -500px; }
-.emoji-26BE { background-position: -800px -520px; }
-.emoji-26C4 { background-position: -800px -540px; }
-.emoji-26C5 { background-position: -800px -560px; }
-.emoji-26C8 { background-position: -800px -580px; }
-.emoji-26CE { background-position: -800px -600px; }
-.emoji-26CF { background-position: -800px -620px; }
-.emoji-26D1 { background-position: -800px -640px; }
-.emoji-26D3 { background-position: -800px -660px; }
-.emoji-26D4 { background-position: -800px -680px; }
-.emoji-26E9 { background-position: -800px -700px; }
-.emoji-26EA { background-position: -800px -720px; }
-.emoji-26F0 { background-position: -800px -740px; }
-.emoji-26F1 { background-position: -800px -760px; }
-.emoji-26F2 { background-position: -800px -780px; }
-.emoji-26F3 { background-position: 0 -800px; }
-.emoji-26F4 { background-position: -20px -800px; }
-.emoji-26F5 { background-position: -40px -800px; }
-.emoji-26F7 { background-position: -60px -800px; }
-.emoji-26F8 { background-position: -80px -800px; }
-.emoji-26F9 { background-position: -100px -800px; }
-.emoji-26F9-1F3FB { background-position: -120px -800px; }
-.emoji-26F9-1F3FC { background-position: -140px -800px; }
-.emoji-26F9-1F3FD { background-position: -160px -800px; }
-.emoji-26F9-1F3FE { background-position: -180px -800px; }
-.emoji-26F9-1F3FF { background-position: -200px -800px; }
-.emoji-26FA { background-position: -220px -800px; }
-.emoji-26FD { background-position: -240px -800px; }
-.emoji-2702 { background-position: -260px -800px; }
-.emoji-2705 { background-position: -280px -800px; }
-.emoji-2708 { background-position: -300px -800px; }
-.emoji-2709 { background-position: -320px -800px; }
-.emoji-270A { background-position: -340px -800px; }
-.emoji-270A-1F3FB { background-position: -360px -800px; }
-.emoji-270A-1F3FC { background-position: -380px -800px; }
-.emoji-270A-1F3FD { background-position: -400px -800px; }
-.emoji-270A-1F3FE { background-position: -420px -800px; }
-.emoji-270A-1F3FF { background-position: -440px -800px; }
-.emoji-270B { background-position: -460px -800px; }
-.emoji-270B-1F3FB { background-position: -480px -800px; }
-.emoji-270B-1F3FC { background-position: -500px -800px; }
-.emoji-270B-1F3FD { background-position: -520px -800px; }
-.emoji-270B-1F3FE { background-position: -540px -800px; }
-.emoji-270B-1F3FF { background-position: -560px -800px; }
-.emoji-270C { background-position: -580px -800px; }
-.emoji-270C-1F3FB { background-position: -600px -800px; }
-.emoji-270C-1F3FC { background-position: -620px -800px; }
-.emoji-270C-1F3FD { background-position: -640px -800px; }
-.emoji-270C-1F3FE { background-position: -660px -800px; }
-.emoji-270C-1F3FF { background-position: -680px -800px; }
-.emoji-270D { background-position: -700px -800px; }
-.emoji-270D-1F3FB { background-position: -720px -800px; }
-.emoji-270D-1F3FC { background-position: -740px -800px; }
-.emoji-270D-1F3FD { background-position: -760px -800px; }
-.emoji-270D-1F3FE { background-position: -780px -800px; }
-.emoji-270D-1F3FF { background-position: -800px -800px; }
-.emoji-270F { background-position: -820px 0; }
-.emoji-2712 { background-position: -820px -20px; }
-.emoji-2714 { background-position: -820px -40px; }
-.emoji-2716 { background-position: -820px -60px; }
-.emoji-271D { background-position: -820px -80px; }
-.emoji-2721 { background-position: -820px -100px; }
-.emoji-2728 { background-position: -820px -120px; }
-.emoji-2733 { background-position: -820px -140px; }
-.emoji-2734 { background-position: -820px -160px; }
-.emoji-2744 { background-position: -820px -180px; }
-.emoji-2747 { background-position: -820px -200px; }
-.emoji-274C { background-position: -820px -220px; }
-.emoji-274E { background-position: -820px -240px; }
-.emoji-2753 { background-position: -820px -260px; }
-.emoji-2754 { background-position: -820px -280px; }
-.emoji-2755 { background-position: -820px -300px; }
-.emoji-2757 { background-position: -820px -320px; }
-.emoji-2763 { background-position: -820px -340px; }
-.emoji-2764 { background-position: -820px -360px; }
-.emoji-2795 { background-position: -820px -380px; }
-.emoji-2796 { background-position: -820px -400px; }
-.emoji-2797 { background-position: -820px -420px; }
-.emoji-27A1 { background-position: -820px -440px; }
-.emoji-27B0 { background-position: -820px -460px; }
-.emoji-27BF { background-position: -820px -480px; }
-.emoji-2934 { background-position: -820px -500px; }
-.emoji-2935 { background-position: -820px -520px; }
-.emoji-2B05 { background-position: -820px -540px; }
-.emoji-2B06 { background-position: -820px -560px; }
-.emoji-2B07 { background-position: -820px -580px; }
-.emoji-2B1B { background-position: -820px -600px; }
-.emoji-2B1C { background-position: -820px -620px; }
-.emoji-2B50 { background-position: -820px -640px; }
-.emoji-2B55 { background-position: -820px -660px; }
-.emoji-3030 { background-position: -820px -680px; }
-.emoji-303D { background-position: -820px -700px; }
-.emoji-3297 { background-position: -820px -720px; }
-.emoji-3299 { background-position: -820px -740px; }
+.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');
@@ -1731,6 +1804,6 @@
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
background-image: image-url('emoji@2x.png');
- background-size: 840px 820px;
+ background-size: 860px 840px;
}
}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index e160d676e35..55f9d4a0011 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,5 +1,35 @@
.environments {
+
.commit-title {
margin: 0;
}
+
+ .fa-play {
+ font-size: 14px;
+ }
+
+ .dropdown-new {
+ color: $table-text-gray;
+ }
+
+ .dropdown-menu {
+
+ .fa {
+ margin-right: 6px;
+ color: $table-text-gray;
+ }
+ }
+
+ .branch-name {
+ color: $gl-dark-link-color;
+ }
+}
+
+.table.builds.environments {
+ min-width: 500px;
+
+ .icon-container {
+ width: 20px;
+ text-align: center;
+ }
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index a2145956eb5..0cd45fb90bf 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -115,11 +115,8 @@
}
&.commits-stat {
- margin-top: 3px;
display: block;
- padding: 3px;
- padding-left: 0;
-
+ padding: 0 3px 0 0;
&:hover {
background: none;
}
@@ -176,3 +173,11 @@
}
}
}
+
+// hide event scope (namespace + project) where it is not necessary
+.project-activity {
+ .event-scope {
+ display: none;
+ }
+}
+
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 701b9388454..b657ca47d38 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -23,48 +23,15 @@
}
.group-row {
- &.no-description {
- .group-name {
- line-height: 44px;
- }
- }
-
.stats {
float: right;
- line-height: 44px;
+ line-height: $list-text-height;
color: $gl-gray;
span {
margin-right: 15px;
}
}
-
- &.group-admin {
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
-
- .group-avatar, .group-details, .group-controls {
- display: -webkit-flex;
- display: -ms-flexbox;
- display: flex;
- }
-
- .group-details {
- flex: 1 1 auto;
- flex-direction: column;
- min-width: 0;
- }
-
- .group-controls {
- align-items: center;
-
- a {
- margin-left: 5px;
- }
- }
- }
-
}
.ldap-group-links {
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 84cc35239f9..a4f76a9495a 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,22 +1,3 @@
-i.icon-gitorious {
- display: inline-block;
- background-position: 0 0;
- background-size: contain;
- background-repeat: no-repeat;
-}
-
-i.icon-gitorious-small {
- background-image: image-url('gitorious-logo-blue.png');
- width: 13px;
- height: 13px;
-}
-
-i.icon-gitorious-big {
- background-image: image-url('gitorious-logo-black.png');
- width: 18px;
- height: 18px;
-}
-
.import-jobs-from-col,
.import-jobs-to-col {
width: 40%;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 542fa244689..46c4a11aa2e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -122,7 +122,8 @@
button {
float: right;
- padding: 3px 5px;
+ padding: 1px 5px;
+ background-color: $gray-light;
}
}
@@ -268,7 +269,7 @@
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
-
+
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
@@ -394,3 +395,12 @@
display: inline-block;
line-height: 18px;
}
+
+.js-issuable-selector-wrap {
+ .js-issuable-selector {
+ width: 100%;
+ }
+ @media (max-width: $screen-sm-max) {
+ margin-bottom: $gl-padding;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 4e35ca329e4..dfe1e3075da 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -63,8 +63,8 @@ form.edit-issue {
.merge-request,
.issue {
&.today {
- background: #efe;
- border-color: #cec;
+ background: #f3fff2;
+ border-color: #e1e8d5;
}
&.closed {
@@ -78,6 +78,14 @@ form.edit-issue {
}
}
+.merge-request-ci-status {
+ svg {
+ margin-right: 4px;
+ position: relative;
+ top: 1px;
+ }
+}
+
@media (max-width: $screen-xs-max) {
.issue-btn-group {
width: 100%;
@@ -91,3 +99,33 @@ form.edit-issue {
.issue-form .select2-container {
width: 250px !important;
}
+
+.issues-footer {
+ padding-top: $gl-padding;
+ padding-bottom: 37px;
+}
+
+.issue-email-modal-btn {
+ padding: 0;
+ color: $gl-link-color;
+ background-color: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.email-modal-input-group {
+ margin-bottom: 10px;
+
+ .form-control {
+ background-color: $white-light;
+ }
+
+ .btn {
+ background-color: $background-color;
+ border: 1px solid $border-gray-light;
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 47bfd144930..606459f82cd 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -162,9 +162,15 @@
}
.filtered-labels {
+ font-size: 0;
+ padding: 12px 16px;
+
.label-row {
+ margin-top: 4px;
+ margin-bottom: 4px;
+
&:not(:last-child) {
- margin-right: 5px;
+ margin-right: 8px;
}
}
@@ -176,6 +182,17 @@
.btn {
color: inherit;
}
+
+ a.btn {
+ padding: 0;
+
+ .has-tooltip {
+ top: 0;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ line-height: 1.1;
+ }
+ }
}
.label-options-toggle {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
new file mode 100644
index 00000000000..1f499897c16
--- /dev/null
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -0,0 +1,238 @@
+$colors: (
+ white_header_head_neutral : #e1fad7,
+ white_line_head_neutral : #effdec,
+ white_button_head_neutral : #9adb84,
+
+ white_header_head_chosen : #baf0a8,
+ white_line_head_chosen : #e1fad7,
+ white_button_head_chosen : #52c22d,
+
+ white_header_origin_neutral : #e0f0ff,
+ white_line_origin_neutral : #f2f9ff,
+ white_button_origin_neutral : #87c2fa,
+
+ white_header_origin_chosen : #add8ff,
+ white_line_origin_chosen : #e0f0ff,
+ white_button_origin_chosen : #268ced,
+
+ white_header_not_chosen : #f0f0f0,
+ white_line_not_chosen : #f9f9f9,
+
+
+ dark_header_head_neutral : rgba(#3f3, .2),
+ dark_line_head_neutral : rgba(#3f3, .1),
+ dark_button_head_neutral : #40874f,
+
+ dark_header_head_chosen : rgba(#3f3, .33),
+ dark_line_head_chosen : rgba(#3f3, .2),
+ dark_button_head_chosen : #258537,
+
+ dark_header_origin_neutral : rgba(#2878c9, .4),
+ dark_line_origin_neutral : rgba(#2878c9, .3),
+ dark_button_origin_neutral : #2a5c8c,
+
+ dark_header_origin_chosen : rgba(#2878c9, .6),
+ dark_line_origin_chosen : rgba(#2878c9, .4),
+ dark_button_origin_chosen : #1d6cbf,
+
+ dark_header_not_chosen : rgba(#fff, .25),
+ dark_line_not_chosen : rgba(#fff, .1),
+
+
+ monokai_header_head_neutral : rgba(#a6e22e, .25),
+ monokai_line_head_neutral : rgba(#a6e22e, .1),
+ monokai_button_head_neutral : #376b20,
+
+ monokai_header_head_chosen : rgba(#a6e22e, .4),
+ monokai_line_head_chosen : rgba(#a6e22e, .25),
+ monokai_button_head_chosen : #39800d,
+
+ monokai_header_origin_neutral : rgba(#60d9f1, .35),
+ monokai_line_origin_neutral : rgba(#60d9f1, .15),
+ monokai_button_origin_neutral : #38848c,
+
+ monokai_header_origin_chosen : rgba(#60d9f1, .5),
+ monokai_line_origin_chosen : rgba(#60d9f1, .35),
+ monokai_button_origin_chosen : #3ea4b2,
+
+ monokai_header_not_chosen : rgba(#76715d, .24),
+ monokai_line_not_chosen : rgba(#76715d, .1),
+
+
+ solarized_light_header_head_neutral : rgba(#859900, .37),
+ solarized_light_line_head_neutral : rgba(#859900, .2),
+ solarized_light_button_head_neutral : #afb262,
+
+ solarized_light_header_head_chosen : rgba(#859900, .5),
+ solarized_light_line_head_chosen : rgba(#859900, .37),
+ solarized_light_button_head_chosen : #94993d,
+
+ solarized_light_header_origin_neutral : rgba(#2878c9, .37),
+ solarized_light_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_light_button_origin_neutral : #60a1bf,
+
+ solarized_light_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_light_line_origin_chosen : rgba(#2878c9, .37),
+ solarized_light_button_origin_chosen : #2482b2,
+
+ solarized_light_header_not_chosen : rgba(#839496, .37),
+ solarized_light_line_not_chosen : rgba(#839496, .2),
+
+
+ solarized_dark_header_head_neutral : rgba(#859900, .35),
+ solarized_dark_line_head_neutral : rgba(#859900, .15),
+ solarized_dark_button_head_neutral : #376b20,
+
+ solarized_dark_header_head_chosen : rgba(#859900, .5),
+ solarized_dark_line_head_chosen : rgba(#859900, .35),
+ solarized_dark_button_head_chosen : #39800d,
+
+ solarized_dark_header_origin_neutral : rgba(#2878c9, .35),
+ solarized_dark_line_origin_neutral : rgba(#2878c9, .15),
+ solarized_dark_button_origin_neutral : #086799,
+
+ solarized_dark_header_origin_chosen : rgba(#2878c9, .6),
+ solarized_dark_line_origin_chosen : rgba(#2878c9, .35),
+ solarized_dark_button_origin_chosen : #0082cc,
+
+ solarized_dark_header_not_chosen : rgba(#839496, .25),
+ solarized_dark_line_not_chosen : rgba(#839496, .15)
+);
+
+
+@mixin color-scheme($color) {
+ .header.line_content, .diff-line-num {
+ &.origin {
+ background-color: map-get($colors, #{$color}_header_origin_neutral);
+ border-color: map-get($colors, #{$color}_header_origin_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_origin_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_origin_chosen);
+ border-color: map-get($colors, #{$color}_header_origin_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_origin_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_origin_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_origin_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_origin_neutral);
+ }
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_header_head_neutral);
+ border-color: map-get($colors, #{$color}_header_head_neutral);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_neutral);
+ border-color: darken(map-get($colors, #{$color}_button_head_neutral), 15);
+ }
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_header_head_chosen);
+ border-color: map-get($colors, #{$color}_header_head_chosen);
+
+ button {
+ background-color: map-get($colors, #{$color}_button_head_chosen);
+ border-color: darken(map-get($colors, #{$color}_button_head_chosen), 15);
+ }
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_header_not_chosen);
+ border-color: map-get($colors, #{$color}_header_not_chosen);
+
+ button {
+ background-color: lighten(map-get($colors, #{$color}_button_head_neutral), 15);
+ border-color: map-get($colors, #{$color}_button_head_neutral);
+ }
+ }
+ }
+ }
+
+ .line_content {
+ &.origin {
+ background-color: map-get($colors, #{$color}_line_origin_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_origin_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ &.head {
+ background-color: map-get($colors, #{$color}_line_head_neutral);
+
+ &.selected {
+ background-color: map-get($colors, #{$color}_line_head_chosen);
+ }
+
+ &.unselected {
+ background-color: map-get($colors, #{$color}_line_not_chosen);
+ }
+ }
+ }
+}
+
+
+#conflicts {
+
+ .white {
+ @include color-scheme('white')
+ }
+
+ .dark {
+ @include color-scheme('dark')
+ }
+
+ .monokai {
+ @include color-scheme('monokai')
+ }
+
+ .solarized-light {
+ @include color-scheme('solarized_light')
+ }
+
+ .solarized-dark {
+ @include color-scheme('solarized_dark')
+ }
+
+ .diff-wrap-lines .line_content {
+ white-space: normal;
+ min-height: 19px;
+ }
+
+ .line_content.header {
+ position: relative;
+
+ button {
+ border-radius: 2px;
+ font-size: 10px;
+ position: absolute;
+ right: 10px;
+ padding: 0;
+ outline: none;
+ color: #fff;
+ width: 75px; // static width to make 2 buttons have same width
+ height: 19px;
+ }
+ }
+
+ .btn-success .fa-spinner {
+ color: #fff;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index d9756b66af0..7fdd79fa8b9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -60,12 +60,27 @@
.ci_widget {
border-bottom: 1px solid #eef0f2;
- i {
+ svg {
margin-right: 4px;
+ position: relative;
+ top: 1px;
+ overflow: visible;
}
&.ci-success {
color: $gl-success;
+
+ a.environment {
+ color: inherit;
+ }
+ }
+
+ &.ci-success_with_warnings {
+ color: $gl-success;
+
+ i {
+ color: $gl-warning;
+ }
}
&.ci-skipped {
@@ -73,11 +88,14 @@
color: #888;
}
- &.ci-pending,
- &.ci-running {
+ &.ci-pending {
color: $gl-warning;
}
+ &.ci-running {
+ color: $blue-normal;
+ }
+
&.ci-failed,
&.ci-error {
color: $gl-danger;
@@ -112,7 +130,6 @@
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
}
-
}
p:last-child {
@@ -193,6 +210,21 @@
.merge-request-title {
margin-bottom: 2px;
+
+ .ci-status-link {
+
+ svg {
+ height: 16px;
+ width: 16px;
+ position: relative;
+ top: 3px;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+ }
}
}
@@ -237,7 +269,7 @@
.builds {
.table-holder {
- overflow-x: scroll;
+ overflow-x: auto;
}
}
@@ -267,7 +299,7 @@
.item-title {
@media (min-width: $screen-sm-min) {
- width: 49%;
+ width: 45%;
}
}
@@ -342,3 +374,20 @@
}
}
}
+
+.mr-version-switch {
+ background: $background-color;
+ padding: $gl-btn-padding;
+ color: $gl-placeholder-color;
+
+ a.btn-link {
+ color: $gl-dark-link-color;
+ }
+}
+
+.merge-request-details {
+
+ .title {
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3784010348a..bd875b9823f 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,32 @@
}
}
+.discussion-with-resolve-btn {
+ display: table;
+ width: 100%;
+ border-collapse: separate;
+ table-layout: auto;
+
+ .btn-group {
+ display: table-cell;
+ float: none;
+ width: 1%;
+
+ &:first-child {
+ width: 100%;
+ padding-right: 5px;
+ }
+
+ &:last-child {
+ padding-left: 5px;
+ }
+ }
+
+ .btn {
+ width: 100%;
+ }
+}
+
.discussion-notes-count {
font-size: 16px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index ac8c02b59dc..54124a3d658 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -91,34 +91,11 @@ ul.notes {
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
- // On diffs code should wrap nicely and not overflow
- code {
- white-space: pre-wrap;
- }
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
-
- hr {
- // Darken 'whitesmoke' a bit to make it more visible in note bodies
- border-color: darken(#f5f5f5, 8%);
- margin: 10px 0;
- }
-
- code {
- word-break: keep-all;
- }
-
- // Border around images in issue and MR comments.
- img:not(.emoji) {
- border: 1px solid $table-border-gray;
- padding: 5px;
- margin: 5px 0;
- max-height: calc(100vh - 100px);
- }
}
}
@@ -304,19 +281,13 @@ ul.notes {
font-size: 17px;
}
- &.js-note-delete {
- i {
- &:hover {
- color: $gl-text-red;
- }
+ &:hover {
+ .danger-highlight {
+ color: $gl-text-red;
}
- }
- &.js-note-edit {
- i {
- &:hover {
- color: $gl-link-color;
- }
+ .link-highlight {
+ color: $gl-link-color;
}
}
}
@@ -406,3 +377,80 @@ ul.notes {
color: $gl-link-color;
}
}
+
+.line-resolve-all-container {
+ .btn-group {
+ margin-top: -1px;
+ margin-left: -4px;
+ }
+
+ .discussion-next-btn {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.line-resolve-all {
+ display: inline-block;
+ padding: 5px 10px;
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ &.has-next-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ .line-resolve-btn {
+ vertical-align: middle;
+ margin-right: 5px;
+ }
+}
+
+.line-resolve-text {
+ vertical-align: middle;
+}
+
+.line-resolve-btn {
+ display: inline-block;
+ position: relative;
+ top: 2px;
+ padding: 0;
+ background-color: transparent;
+ border: none;
+ outline: 0;
+
+ &.is-disabled {
+ cursor: default;
+ }
+
+ &:not(.is-disabled):hover,
+ &:not(.is-disabled):focus,
+ &.is-active {
+ color: $gl-text-green;
+
+ svg path {
+ fill: $gl-text-green;
+ }
+ }
+
+ svg {
+ position: relative;
+ color: $notes-action-color;
+
+ path {
+ fill: $notes-action-color;
+ }
+ }
+}
+
+.discussion-next-btn {
+ svg {
+ margin: 0;
+
+ path {
+ fill: $gray-darkest;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 6128868b670..0dcf61dd2dd 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,15 +1,12 @@
.pipelines {
.stage {
- max-width: 100px;
+ max-width: 90px;
+ width: 90px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
- .duration, .finished_at {
- margin: 4px 0;
- }
-
.commit-title {
margin: 0;
}
@@ -21,4 +18,462 @@
.btn {
margin: 4px;
}
+
+ .table.builds {
+ min-width: 1200px;
+ }
+}
+
+.content-list {
+
+ &.pipelines,
+ &.builds-content-list {
+ width: 100%;
+ overflow: auto;
+ }
+}
+
+.pipeline-holder {
+ width: 100%;
+ overflow: auto;
+}
+
+.table.builds {
+ min-width: 900px;
+
+ &.pipeline {
+ min-width: 650px;
+ }
+
+ tr {
+ th {
+ padding: 16px 8px;
+ border: none;
+ }
+
+ td {
+ padding: 10px 8px;
+ }
+ }
+
+ tbody {
+ border-top-width: 1px;
+ }
+
+ .commit-link {
+
+ .ci-status {
+
+ svg {
+ top: 1px;
+ margin-right: 0;
+ }
+ }
+
+ a:hover {
+ text-decoration: none;
+ }
+ }
+
+ .branch-commit {
+
+ .branch-name {
+ font-weight: bold;
+ max-width: 150px;
+ overflow: hidden;
+ display: inline-block;
+ white-space: nowrap;
+ vertical-align: top;
+ text-overflow: ellipsis;
+ }
+
+ svg {
+ height: 14px;
+ width: 14px;
+ vertical-align: middle;
+ fill: $table-text-gray;
+ }
+
+ .fa {
+ font-size: 12px;
+ color: $table-text-gray;
+ }
+
+ .commit-id {
+ color: $gl-link-color;
+ margin-right: 8px;
+ }
+
+ .commit-title {
+ margin-top: 4px;
+ max-width: 300px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .avatar {
+ margin-left: 0;
+ }
+
+ .label {
+ margin-right: 4px;
+ }
+
+ .label-container {
+ font-size: 0;
+
+ .label {
+ margin-top: 5px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ text-align: right;
+ width: 15px;
+
+ .fa {
+ position: relative;
+ right: 3px;
+ }
+
+ svg {
+ position: relative;
+ right: 1px;
+ }
+ }
+
+ .stage-cell {
+
+ svg {
+ height: 18px;
+ width: 18px;
+ vertical-align: middle;
+ overflow: visible;
+ }
+
+ .light {
+ width: 3px;
+ }
+ }
+
+ .duration,
+ .finished-at {
+ color: $table-text-gray;
+ margin: 4px 0;
+
+ .fa {
+ font-size: 12px;
+ margin-right: 4px;
+ }
+
+ svg {
+ width: 12px;
+ height: 12px;
+ vertical-align: middle;
+ margin-right: 4px;
+ }
+ }
+
+ .pipeline-actions {
+ min-width: 140px;
+
+ .btn {
+ margin: 0;
+ color: $table-text-gray;
+ }
+
+ .cancel-retry-btns {
+ vertical-align: middle;
+
+ .btn:not(:first-child) {
+ margin-left: 8px;
+ }
+ }
+
+ .dropdown-toggle,
+ .dropdown-menu {
+ color: $table-text-gray;
+
+ .fa {
+ color: $table-text-gray;
+ margin-right: 6px;
+ font-size: 14px;
+ }
+ }
+
+ .btn-remove {
+ color: $white-light;
+ }
+
+ .btn-group {
+ &.open {
+ .btn-default {
+ background-color: $white-normal;
+ border-color: $border-white-normal;
+ }
+ }
+ }
+ }
+
+ .build-link {
+
+ a {
+ color: $gl-dark-link-color;
+ }
+ }
+
+ .btn-group.open .dropdown-toggle {
+ box-shadow: none;
+ }
+}
+
+// Pipeline visualization
+
+.toggle-pipeline-btn {
+ background-color: $gray-dark;
+
+ .caret {
+ border-top: none;
+ border-bottom: 4px solid;
+ }
+
+ &.graph-collapsed {
+ background-color: $white-light;
+
+ .caret {
+ border-bottom: none;
+ border-top: 4px solid;
+ }
+ }
+}
+
+.pipeline-graph {
+ width: 100%;
+ 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;
+ margin-right: 65px;
+
+ li {
+ list-style: none;
+ }
+
+ .stage-name {
+ margin-bottom: 15px;
+ font-weight: bold;
+ width: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .build {
+ border: 1px solid $border-color;
+ position: relative;
+ padding: 6px 10px;
+ border-radius: 30px;
+ width: 150px;
+ margin-bottom: 10px;
+
+ &.playable {
+ background-color: $gray-light;
+
+ svg {
+ height: 12px;
+ width: 12px;
+ position: relative;
+ top: 1px;
+
+ path {
+ fill: $layout-link-gray;
+ }
+ }
+ }
+
+ .build-content {
+ width: 130px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ a {
+ color: $layout-link-gray;
+ text-decoration: none;
+
+ &:hover {
+ .ci-status-text {
+ text-decoration: underline;
+ }
+ }
+
+ }
+ }
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ }
+
+ // Connect first build in each stage with right horizontal line
+ &:first-child {
+ &::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+ right: -69px;
+ border-top: 2px solid $border-color;
+ width: 69px;
+ height: 1px;
+ }
+ }
+
+ // Connect each build (except for first) with curved lines
+ &:not(:first-child) {
+ &::after, &::before {
+ content: '';
+ top: -47px;
+ position: absolute;
+ border-bottom: 2px solid $border-color;
+ width: 20px;
+ height: 65px;
+ }
+
+ // Right connecting curves
+ &::after {
+ right: -20px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 0 15px;
+ }
+
+ // Left connecting curves
+ &::before {
+ left: -20px;
+ border-left: 2px solid $border-color;
+ border-radius: 0 0 0 15px;
+ }
+ }
+
+ // Connect second build to first build with smaller curved line
+ &:nth-child(2) {
+ &::after, &::before {
+ height: 29px;
+ top: -10px;
+ }
+ .curve {
+ display: block;
+ }
+ }
+ }
+
+ &:last-child {
+ .build {
+ // Remove right connecting horizontal line from first build in last stage
+ &:first-child {
+ &::after, &::before {
+ 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: -28.5px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -39px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 15px;
+ }
+
+ &::before {
+ right: -39px;
+ border-left: 2px solid $border-color;
+ border-radius: 15px 0 0;
+ }
+ }
+}
+
+.pipeline-actions {
+ border-bottom: none;
+}
+
+.toggle-pipeline-btn {
+
+ .fa {
+ color: $dropdown-header-color;
+ }
+}
+
+.pipelines.tab-pane {
+
+ .content-list.pipelines {
+ overflow: scroll;
+ }
+
+ .stage {
+ max-width: 60px;
+ width: 60px;
+ }
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 46371ec6871..6f58203f49c 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -228,3 +228,9 @@
}
}
}
+
+table.u2f-registrations {
+ th:not(:last-child), td:not(:last-child) {
+ border-right: solid 1px transparent;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bce4aac3334..eaf2d3270b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -99,7 +99,7 @@
margin-left: auto;
margin-right: auto;
margin-bottom: 15px;
- max-width: 480px;
+ max-width: 700px;
> p {
margin-bottom: 0;
@@ -129,6 +129,17 @@
color: $layout-link-gray;
}
+ svg {
+
+ path {
+ fill: $layout-link-gray;
+ }
+
+ use {
+ stroke: $layout-link-gray;
+ }
+ }
+
.fa-caret-down {
margin-left: 3px;
}
@@ -322,41 +333,83 @@ a.deploy-project-label {
}
.fork-namespaces {
- .fork-thumbnail {
- text-align: center;
- margin-bottom: $gl-padding;
-
- .caption {
- padding: $gl-padding 0;
- min-height: 30px;
- }
+ .row {
+ -webkit-flex-wrap: wrap;
+ display: -webkit-flex;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+
+ .fork-thumbnail {
+ @include border-radius($border-radius-base);
+ background-color: $white-light;
+ border: 1px solid $border-white-light;
+ height: 202px;
+ margin: $gl-padding;
+ text-align: center;
+ width: 169px;
+ &:hover, &.forked {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
+ }
+ .no-avatar {
+ width: 100px;
+ height: 100px;
+ background-color: $gray-light;
+ border: 1px solid $gray-dark;
+ margin: 0 auto;
+ @include border-radius(50%);
+ i {
+ font-size: 100px;
+ color: $gray-dark;
+ }
+ }
+ a {
+ display: block;
+ width: 100%;
+ height: 100%;
+ padding-top: $gl-padding;
+ color: $gl-gray;
+ .caption {
+ min-height: 30px;
+ padding: $gl-padding 0;
+ }
+ }
- img {
- @include border-radius(50%);
- max-width: 100px;
+ img {
+ @include border-radius(50%);
+ max-width: 100px;
+ }
}
}
}
.project-import {
.form-group {
- margin-bottom: 0;
+ margin-bottom: 5px;
}
+
.import-buttons {
padding-left: 0;
display: -webkit-flex;
display: flex;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
+
.btn {
- margin-right: 10px;
- padding: 8px 12px;
+ margin: 0 10px 10px 0;
+ padding: 8px;
}
- &> div {
- margin-bottom: 14px;
+
+ > div {
padding-left: 0;
+
&:last-child {
margin-bottom: 0;
+
+ .btn {
+ margin-right: 0;
+ }
}
}
}
@@ -459,22 +512,25 @@ pre.light-well {
.project-row {
border-color: $table-border-color;
- &.no-description {
- .project {
- line-height: 40px;
- }
- }
-
.project-full-name {
@include str-truncated;
}
.controls {
- line-height: 40px;
+ line-height: $list-text-height;
a:hover {
text-decoration: none;
}
+
+ > span {
+ margin-left: 10px;
+ }
+
+ svg {
+ position: relative;
+ top: 2px;
+ }
}
}
@@ -599,14 +655,39 @@ pre.light-well {
}
}
+.new_protected_branch {
+ label {
+ margin-top: 6px;
+ font-weight: normal;
+ }
+}
+
.protected-branches-list {
a {
color: $gl-gray;
- font-weight: 600;
&:hover {
color: $gl-link-color;
}
+
+ &.is-active {
+ font-weight: 600;
+ }
+ }
+
+ .settings-message {
+ margin: 0;
+ border-radius: 0 0 1px 1px;
+ padding: 20px 0;
+ border: none;
+ }
+
+ .table-bordered {
+ border-radius: 1px;
+
+ th:not(:last-child), td:not(:last-child) {
+ border-right: solid 1px transparent;
+ }
}
}
@@ -632,3 +713,35 @@ pre.light-well {
width: 300px;
}
}
+
+.compare-form-group {
+ .dropdown-menu {
+ width: 300px;
+ }
+}
+
+.clearable-input {
+ position: relative;
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ &.has-value {
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 9e9b18fdbb8..c9d436d72ba 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -185,7 +185,7 @@
padding-right: $gl-padding + 15px;
}
- .btn-search {
+ .btn-search, .btn-new {
width: 100%;
margin-top: 5px;
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 2370d35924e..587f2d9f3c1 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -15,7 +15,8 @@
border-color: $gl-danger;
}
- &.ci-success {
+ &.ci-success,
+ &.ci-success_with_warnings {
color: $gl-success;
border-color: $gl-success;
}
@@ -32,11 +33,24 @@
border-color: $gl-gray;
}
- &.ci-pending,
- &.ci-running {
+ &.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
}
+
+ &.ci-running {
+ color: $blue-normal;
+ border-color: $blue-normal;
+ }
+
+ svg {
+ height: 13px;
+ width: 13px;
+ position: relative;
+ top: 1px;
+ margin: 0 3px;
+ overflow: visible;
+ }
}
.ci-status-icon-success {
@@ -45,10 +59,15 @@
.ci-status-icon-failed {
color: $gl-danger;
}
- .ci-status-icon-running,
- .ci-status-icon-pending {
+
+ .ci-status-icon-pending,
+ .ci-status-icon-success_with_warning {
color: $gl-warning;
}
+
+ .ci-status-icon-running {
+ color: $blue-normal;
+ }
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
@@ -56,3 +75,11 @@
color: $gl-gray;
}
}
+
+.visible-xs-inline {
+ .ci-status-link {
+ position: relative;
+ top: 2px;
+ left: 5px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 00000000000..24ebd3e7cfa
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,7 @@
+.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 cf16d070cfe..0340526a53a 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -20,10 +20,43 @@
}
}
-.todo {
+.todos-list > .todo {
+ // workaround because we cannot use border-colapse
+ border-top: 1px solid transparent;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: row;
+ flex-direction: row;
+
&:hover {
+ background-color: $row-hover;
+ border-color: $row-hover-border;
cursor: pointer;
}
+
+ // overwrite border style of .content-list
+ &:last-child {
+ border-bottom: 1px solid transparent;
+
+ &:hover {
+ border-color: $row-hover-border;
+ }
+ }
+
+ .todo-actions {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: center;
+ justify-content: center;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ margin-left: 10px;
+ }
+
+ .todo-item {
+ -webkit-flex: auto;
+ flex: auto;
+ }
}
.todo-item {
@@ -43,8 +76,6 @@
}
.todo-body {
- margin-right: 174px;
-
.todo-note {
word-wrap: break-word;
@@ -90,6 +121,12 @@
}
@media (max-width: $screen-xs-max) {
+ .todo {
+ .avatar {
+ display: none;
+ }
+ }
+
.todo-item {
.todo-title {
white-space: normal;
@@ -98,10 +135,6 @@
margin-bottom: 10px;
}
- .avatar {
- display: none;
- }
-
.todo-body {
margin: 0;
border-left: 2px solid #ddd;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 5b61270daa8..9da40fe2b09 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -19,16 +19,15 @@
border-top: 1px solid $table-border-gray;
td, th {
- line-height: 23px;
+ line-height: 21px;
}
&:hover {
- cursor: pointer;
-
td {
background-color: $row-hover;
border-top: 1px solid $row-hover-border;
border-bottom: 1px solid $row-hover-border;
+ cursor: pointer;
}
}
@@ -59,6 +58,10 @@
.tree_commit {
max-width: 320px;
+
+ .str-truncated {
+ max-width: 100%;
+ }
}
.tree_time_ago {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 23ba83aba0e..6ef7cf0bae6 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -64,6 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:disabled_oauth_sign_in_sources] =
AuthHelper.button_based_providers.map(&:to_s) -
Array(enabled_oauth_sign_in_sources)
+ params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
:default_projects_limit,
@@ -83,7 +84,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_project_visibility,
:default_snippet_visibility,
:default_group_visibility,
- :restricted_signup_domains_raw,
+ :domain_whitelist_raw,
+ :domain_blacklist_enabled,
+ :domain_blacklist_raw,
+ :domain_blacklist_file,
:version_check_enabled,
:admin_notification_email,
:user_oauth_applications,
@@ -105,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
+ :koding_enabled,
+ :koding_url,
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
index 0db91eaaf2e..88f3c0e2fd4 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/builds_controller.rb
@@ -5,8 +5,10 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
+ when 'pending'
+ @builds.pending.reverse_order
when 'running'
- @builds.running_or_pending.reverse_order
+ @builds.running.reverse_order
when 'finished'
@builds.finished
else
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 94b5aaa71d0..cdfa8d91a28 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,15 +42,15 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
def destroy
- DestroyGroupService.new(@group, current_user).execute
+ DestroyGroupService.new(@group, current_user).async_execute
- redirect_to admin_groups_path, notice: 'Group was successfully deleted.'
+ redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
private
@@ -60,6 +60,6 @@ class Admin::GroupsController < Admin::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level)
+ params.require(:group).permit(:name, :description, :path, :avatar, :visibility_level, :request_access_enabled)
end
end
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 8be35f00a77..9433da02f64 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -7,7 +7,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
warden.set_user(impersonator, scope: :user)
- Gitlab::AppLogger.info("User #{original_user.username} has stopped impersonating #{impersonator.username}")
+ Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
session[:impersonator_id] = nil
diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb
new file mode 100644
index 00000000000..a478176e138
--- /dev/null
+++ b/app/controllers/admin/requests_profiles_controller.rb
@@ -0,0 +1,17 @@
+class Admin::RequestsProfilesController < Admin::ApplicationController
+ def index
+ @profile_token = Gitlab::RequestProfiler.profile_token
+ @profiles = Gitlab::RequestProfiler::Profile.all.group_by(&:request_path)
+ end
+
+ def show
+ clean_name = Rack::Utils.clean_path_info(params[:name])
+ profile = Gitlab::RequestProfiler::Profile.find(clean_name)
+
+ if profile
+ render text: profile.content
+ else
+ redirect_to admin_requests_profiles_path, alert: 'Profile not found'
+ end
+ end
+end
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 46133588332..7c37f3155da 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -1,4 +1,6 @@
class Admin::ServicesController < Admin::ApplicationController
+ include ServiceParams
+
before_action :service, only: [:edit, :update]
def index
@@ -13,7 +15,7 @@ class Admin::ServicesController < Admin::ApplicationController
end
def update
- if service.update_attributes(application_services_params[:service])
+ if service.update_attributes(service_params[:service])
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
else
@@ -37,15 +39,4 @@ class Admin::ServicesController < Admin::ApplicationController
def service
@service ||= Service.where(id: params[:id], template: true).first
end
-
- def application_services_params
- application_services_params = params.permit(:id,
- service: Projects::ServicesController::ALLOWED_PARAMS)
- if application_services_params[:service].is_a?(Hash)
- Projects::ServicesController::FILTER_BLANK_PARAMS.each do |param|
- application_services_params[:service].delete(param) if application_services_params[:service][param].blank?
- end
- end
- application_services_params
- end
end
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 3a2f0185315..2abfa22712d 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
head :ok
end
end
+
+ def mark_as_ham
+ spam_log = SpamLog.find(params[:id])
+
+ if HamService.new(spam_log).mark_as_ham!
+ redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
+ else
+ redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
end
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index e4c73008826..ca04a17caa1 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -29,7 +29,8 @@ class Admin::SystemInfoController < Admin::ApplicationController
]
def show
- system_info = Vmstat.snapshot
+ @cpus = Vmstat.cpu rescue nil
+ @memory = Vmstat.memory rescue nil
mounts = Sys::Filesystem.mounts
@disks = []
@@ -50,10 +51,5 @@ class Admin::SystemInfoController < Admin::ApplicationController
rescue Sys::Filesystem::Error
end
end
-
- @cpus = system_info.cpus.length
-
- @mem_used = system_info.memory.active_bytes
- @mem_total = system_info.memory.total_bytes
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9cc31620d9f..ebc2a4651ba 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include SentryHelper
include WorkhorseHelper
before_action :authenticate_user_from_private_token!
@@ -24,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -46,28 +47,6 @@ class ApplicationController < ActionController::Base
protected
- def sentry_context
- if Rails.env.production? && current_application_settings.sentry_enabled
- if current_user
- Raven.user_context(
- id: current_user.id,
- email: current_user.email,
- username: current_user.username,
- )
- end
-
- Raven.tags_context(program: sentry_program_context)
- end
- end
-
- def sentry_program_context
- if Sidekiq.server?
- 'sidekiq'
- else
- 'rails'
- end
- end
-
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
@@ -243,42 +222,6 @@ class ApplicationController < ActionController::Base
end
end
- def set_filters_params
- set_default_sort
-
- params[:scope] = 'all' if params[:scope].blank?
- params[:state] = 'opened' if params[:state].blank?
-
- @sort = params[:sort]
- @filter_params = params.dup
-
- if @project
- @filter_params[:project_id] = @project.id
- elsif @group
- @filter_params[:group_id] = @group.id
- else
- # TODO: this filter ignore issues/mr created in public or
- # internal repos where you are not a member. Enable this filter
- # or improve current implementation to filter only issues you
- # created or assigned or mentioned
- # @filter_params[:authorized_only] = true
- end
-
- @filter_params
- end
-
- def get_issues_collection
- set_filters_params
- @issuable_finder = IssuesFinder.new(current_user, @filter_params)
- @issuable_finder.execute
- end
-
- def get_merge_requests_collection
- set_filters_params
- @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params)
- @issuable_finder.execute
- end
-
def import_sources_enabled?
!current_application_settings.import_sources.empty?
end
@@ -307,10 +250,6 @@ class ApplicationController < ActionController::Base
Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
end
- def gitorious_import_enabled?
- current_application_settings.import_sources.include?('gitorious')
- end
-
def google_code_import_enabled?
current_application_settings.import_sources.include?('google_code')
end
@@ -344,10 +283,6 @@ class ApplicationController < ActionController::Base
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
- def browser_supports_u2f?
- browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
- end
-
def redirect_to_home_page_url?
# If user is not signed-in and tries to access root_path - redirect him to landing page
# Don't redirect to the default URL to prevent endless redirections
@@ -367,24 +302,4 @@ class ApplicationController < ActionController::Base
def u2f_app_id
request.base_url
end
-
- private
-
- def set_default_sort
- key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests')
- 'issuable_sort'
- end
-
- cookies[key] = params[:sort] if key && params[:sort].present?
- params[:sort] = cookies[key] if key
- params[:sort] ||= 'id_desc'
- end
-
- def is_a_listing_page_for?(page_type)
- controller_name, action_name = params.values_at(:controller, :action)
-
- (controller_name == "projects/#{page_type}" && action_name == 'index') ||
- (controller_name == 'groups' && action_name == page_type) ||
- (controller_name == 'dashboard' && action_name == page_type)
- end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index c89678cf2d8..b48668eea87 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,10 +1,12 @@
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users]
+ before_action :load_project, only: [:users]
before_action :find_users, only: [:users]
def users
@users ||= User.none
@users = @users.search(params[:search]) if params[:search].present?
+ @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
@users = @users.active
@users = @users.reorder(:name)
@users = @users.page(params[:page])
@@ -33,19 +35,13 @@ class AutocompleteController < ApplicationController
def projects
project = Project.find_by_id(params[:project_id])
-
- projects = current_user.authorized_projects
- projects = projects.search(params[:search]) if params[:search].present?
- projects = projects.select do |project|
- current_user.can?(:admin_issue, project)
- end
+ projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
no_project = {
id: 0,
name_with_namespace: 'No project',
}
- projects.unshift(no_project)
- projects.delete(project)
+ projects.unshift(no_project) unless params[:offset_id].present?
render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
end
@@ -54,11 +50,8 @@ class AutocompleteController < ApplicationController
def find_users
@users =
- if params[:project_id].present?
- project = Project.find(params[:project_id])
- return render_404 unless can?(current_user, :read_project, project)
-
- project.team.users
+ if @project
+ @project.team.users
elsif params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
@@ -70,4 +63,18 @@ class AutocompleteController < ApplicationController
User.none
end
end
+
+ def load_project
+ @project ||= begin
+ if params[:project_id].present?
+ project = Project.find(params[:project_id])
+ return render_404 unless can?(current_user, :read_project, project)
+ project
+ end
+ end
+ end
+
+ def projects_finder
+ MoveToProjectFinder.new(current_user)
+ end
end
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 998b8adc411..ba07cea569c 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -57,7 +57,7 @@ module AuthenticatesWithTwoFactor
# Authenticate using the response from a U2F (universal 2nd factor) device
def authenticate_with_two_factor_via_u2f(user)
- if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenges])
+ if U2fRegistration.authenticate(user, u2f_app_id, user_params[:device_response], session[:challenge])
# Remove any lingering user data from login
session.delete(:otp_user_id)
session.delete(:challenges)
@@ -77,11 +77,9 @@ module AuthenticatesWithTwoFactor
if key_handles.present?
sign_requests = u2f.authentication_requests(key_handles)
- challenges = sign_requests.map(&:challenge)
- session[:challenges] = challenges
- gon.push(u2f: { challenges: challenges, app_id: u2f_app_id,
- sign_requests: sign_requests,
- browser_supports_u2f: browser_supports_u2f? })
+ session[:challenge] ||= u2f.challenge
+ gon.push(u2f: { challenge: session[:challenge], app_id: u2f_app_id,
+ sign_requests: sign_requests })
end
end
end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index dacb5679dd3..f2b8f297bc2 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -7,7 +7,8 @@ module CreatesCommit
commit_params = @commit_params.merge(
source_project: @project,
source_branch: @ref,
- target_branch: @target_branch
+ target_branch: @target_branch,
+ previous_path: @previous_path
)
result = service.new(@tree_edit_project, current_user, commit_params).execute
diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb
new file mode 100644
index 00000000000..aeec3009f15
--- /dev/null
+++ b/app/controllers/concerns/diff_for_path.rb
@@ -0,0 +1,24 @@
+module DiffForPath
+ extend ActiveSupport::Concern
+
+ 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]
+ end
+
+ return render_404 unless diff_file
+
+ diff_commit = commit_for_diff(diff_file)
+ blob = diff_file.blob(diff_commit)
+
+ locals = {
+ diff_file: diff_file,
+ diff_commit: diff_commit,
+ diff_refs: diffs.diff_refs,
+ blob: blob,
+ project: project
+ }
+
+ render json: { html: view_to_html_string('projects/diffs/_content', locals) }
+ end
+end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
new file mode 100644
index 00000000000..b5e79099e39
--- /dev/null
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -0,0 +1,84 @@
+module IssuableCollections
+ extend ActiveSupport::Concern
+ include SortingHelper
+
+ included do
+ helper_method :issues_finder
+ helper_method :merge_requests_finder
+ end
+
+ private
+
+ def issues_collection
+ issues_finder.execute
+ end
+
+ def merge_requests_collection
+ merge_requests_finder.execute
+ end
+
+ def issues_finder
+ @issues_finder ||= issuable_finder_for(IssuesFinder)
+ end
+
+ def merge_requests_finder
+ @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
+ end
+
+ def issuable_finder_for(finder_class)
+ finder_class.new(current_user, filter_params)
+ end
+
+ def filter_params
+ set_sort_order_from_cookie
+ set_default_scope
+ set_default_state
+
+ @filter_params = params.dup
+ @filter_params[:sort] ||= default_sort_order
+
+ @sort = @filter_params[:sort]
+
+ if @project
+ @filter_params[:project_id] = @project.id
+ elsif @group
+ @filter_params[:group_id] = @group.id
+ else
+ # TODO: this filter ignore issues/mr created in public or
+ # internal repos where you are not a member. Enable this filter
+ # or improve current implementation to filter only issues you
+ # created or assigned or mentioned
+ # @filter_params[:authorized_only] = true
+ end
+
+ @filter_params
+ end
+
+ def set_default_scope
+ params[:scope] = 'all' if params[:scope].blank?
+ end
+
+ def set_default_state
+ params[:state] = 'opened' if params[:state].blank?
+ end
+
+ def set_sort_order_from_cookie
+ key = 'issuable_sort'
+
+ cookies[key] = params[:sort] if params[:sort].present?
+
+ # id_desc and id_asc are old values for these two.
+ cookies[key] = sort_value_recently_created if cookies[key] == 'id_desc'
+ cookies[key] = sort_value_oldest_created if cookies[key] == 'id_asc'
+
+ params[:sort] = cookies[key]
+ end
+
+ def default_sort_order
+ case params[:state]
+ when 'opened', 'all' then sort_value_recently_created
+ when 'merged', 'closed' then sort_value_recently_updated
+ else sort_value_recently_created
+ end
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
index 4feabc32b1c..b89fb94be6e 100644
--- a/app/controllers/concerns/issues_action.rb
+++ b/app/controllers/concerns/issues_action.rb
@@ -1,12 +1,14 @@
module IssuesAction
extend ActiveSupport::Concern
+ include IssuableCollections
def issues
- @issues = get_issues_collection.non_archived
- @issues = @issues.page(params[:page])
- @issues = @issues.preload(:author, :project)
+ @label = issues_finder.labels.first
- @label = @issuable_finder.labels.first
+ @issues = issues_collection
+ .non_archived
+ .preload(:author, :project)
+ .page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
index 06a6b065e7e..a1b0eee37f9 100644
--- a/app/controllers/concerns/merge_requests_action.rb
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -1,11 +1,13 @@
module MergeRequestsAction
extend ActiveSupport::Concern
+ include IssuableCollections
def merge_requests
- @merge_requests = get_merge_requests_collection.non_archived
- @merge_requests = @merge_requests.page(params[:page])
- @merge_requests = @merge_requests.preload(:author, :target_project)
+ @label = merge_requests_finder.labels.first
- @label = @issuable_finder.labels.first
+ @merge_requests = merge_requests_collection
+ .non_archived
+ .preload(:author, :target_project)
+ .page(params[:page])
end
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
new file mode 100644
index 00000000000..a69877edfd4
--- /dev/null
+++ b/app/controllers/concerns/service_params.rb
@@ -0,0 +1,38 @@
+module ServiceParams
+ extend ActiveSupport::Concern
+
+ ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
+ :room, :recipients, :project_url, :webhook,
+ :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
+ :build_key, :server, :teamcity_url, :drone_url, :build_type,
+ :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
+ :colorize_messages, :channels,
+ # We're using `issues_events` and `merge_requests_events`
+ # in the view so we still need to explicitly state them
+ # here. `Service#event_names` would only give
+ # `issue_events` and `merge_request_events` (singular!)
+ # See app/helpers/services_helper.rb for how we
+ # make those event names plural as special case.
+ :issues_events, :merge_requests_events,
+ :notify_only_broken_builds, :notify_only_broken_pipelines,
+ :add_pusher, :send_from_committer_email, :disable_diffs,
+ :external_wiki_url, :notify, :color,
+ :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
+ :jira_issue_transition_id]
+
+ # Parameters to ignore if no value is specified
+ FILTER_BLANK_PARAMS = [:password]
+
+ def service_params
+ dynamic_params = @service.event_channel_names + @service.event_names
+ service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params)
+
+ if service_params[:service].is_a?(Hash)
+ FILTER_BLANK_PARAMS.each do |param|
+ service_params[:service].delete(param) if service_params[:service][param].blank?
+ end
+ end
+
+ service_params
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
new file mode 100644
index 00000000000..29e243c66a3
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -0,0 +1,25 @@
+module SpammableActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_submit_spammable!, only: :mark_as_spam
+ end
+
+ def mark_as_spam
+ if SpamService.new(spammable).mark_as_spam!
+ redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+ else
+ redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
+
+ private
+
+ def spammable
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+
+ def authorize_submit_spammable!
+ access_denied! unless current_user.admin?
+ end
+end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 3a2db3e6eeb..d425d0f9014 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -1,19 +1,18 @@
class Dashboard::TodosController < Dashboard::ApplicationController
- include TodosHelper
-
before_action :find_todos, only: [:index, :destroy_all]
def index
+ @sort = params[:sort]
@todos = @todos.page(params[:page])
end
def destroy
- TodoService.new.mark_todos_as_done([todo], current_user)
+ TodoService.new.mark_todos_as_done_by_ids([params[:id]], current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'Todo was successfully marked as done.' }
format.js { head :ok }
- format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
+ format.json { render json: todos_counts }
end
end
@@ -23,17 +22,20 @@ class Dashboard::TodosController < Dashboard::ApplicationController
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
- format.json { render json: { count: todos_pending_count, done_count: todos_done_count } }
+ format.json { render json: todos_counts }
end
end
private
- def todo
- @todo ||= find_todos.find(params[:id])
- end
-
def find_todos
@todos ||= TodosFinder.new(current_user, params).execute
end
+
+ def todos_counts
+ {
+ count: current_user.todos_pending_count,
+ done_count: current_user.todos_done_count
+ }
+ end
end
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
index 461fc059a3c..a1ab8b99048 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,5 +1,5 @@
class Explore::ApplicationController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked
+ skip_before_action :authenticate_user!, :reject_blocked!
layout 'explore'
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a12536..272164cd0cc 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ current_user: current_user,
+ expires_at: params[:expires_at]
+ )
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
- params.require(:group_member).permit(:access_level, :user_id)
+ params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index a04bf7df722..cb82d62616c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -87,9 +87,9 @@ class GroupsController < Groups::ApplicationController
end
def destroy
- DestroyGroupService.new(@group, current_user).execute
+ DestroyGroupService.new(@group, current_user).async_execute
- redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted."
+ redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion."
end
protected
@@ -121,7 +121,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public, :visibility_level, :share_with_group_lock, :request_access_enabled)
end
def load_events
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 9b5c43b17e2..4eca278599f 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,5 +1,5 @@
class HelpController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked
+ skip_before_action :authenticate_user!, :reject_blocked!
layout 'help'
@@ -12,13 +12,12 @@ class HelpController < ApplicationController
end
def show
- @category = clean_path_info(path_params[:category])
- @file = path_params[:file]
+ @path = clean_path_info(path_params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
# Note: We are purposefully NOT using `Rails.root.join`
- path = File.join(Rails.root, 'doc', @category, "#{@file}.md")
+ path = File.join(Rails.root, 'doc', "#{@path}.md")
if File.exist?(path)
@markdown = File.read(path)
@@ -31,9 +30,9 @@ class HelpController < ApplicationController
end
# Allow access to images in the doc folder
- format.any(:png, :gif, :jpeg) do
+ format.any(:png, :gif, :jpeg, :mp4) do
# Note: We are purposefully NOT using `Rails.root.join`
- path = File.join(Rails.root, 'doc', @category, "#{@file}.#{params[:format]}")
+ path = File.join(Rails.root, 'doc', "#{@path}.#{params[:format]}")
if File.exist?(path)
send_file(path, disposition: 'inline')
@@ -57,8 +56,7 @@ class HelpController < ApplicationController
private
def path_params
- params.require(:category)
- params.require(:file)
+ params.require(:path)
params
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 25e58724860..944c73d139a 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -82,8 +82,6 @@ class Import::BitbucketController < Import::BaseController
go_to_bitbucket_for_permissions
end
- private
-
def access_params
{
bitbucket_access_token: session[:bitbucket_access_token],
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 23a396e8084..08130ee8176 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -61,8 +61,6 @@ class Import::GitlabController < Import::BaseController
go_to_gitlab_for_permissions
end
- private
-
def access_params
{ gitlab_access_token: session[:gitlab_access_token] }
end
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 30df1fb2fec..7d0eff37635 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,5 +1,6 @@
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
+ before_action :authenticate_admin!
def new
@namespace_id = project_params[:namespace_id]
@@ -12,13 +13,14 @@ class Import::GitlabProjectsController < Import::BaseController
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
- imported_file = project_params[:file].path + "-import"
+ import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename)
- FileUtils.copy_entry(project_params[:file].path, imported_file)
+ FileUtils.mkdir_p(File.dirname(import_upload_path))
+ FileUtils.copy_entry(project_params[:file].path, import_upload_path)
@project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
current_user,
- File.expand_path(imported_file),
+ import_upload_path,
project_params[:path]).execute
if @project.saved?
@@ -46,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
+
+ def authenticate_admin!
+ render_404 unless current_user.is_admin?
+ end
end
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
deleted file mode 100644
index a4c4ad23027..00000000000
--- a/app/controllers/import/gitorious_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class Import::GitoriousController < Import::BaseController
- before_action :verify_gitorious_import_enabled
-
- def new
- redirect_to client.authorize_url(callback_import_gitorious_url)
- end
-
- def callback
- session[:gitorious_repos] = params[:repos]
- redirect_to status_import_gitorious_path
- end
-
- def status
- @repos = client.repos
-
- @already_added_projects = current_user.created_projects.where(import_type: "gitorious")
- already_added_projects_names = @already_added_projects.pluck(:import_source)
-
- @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
- end
-
- def jobs
- jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status])
- render json: jobs
- end
-
- def create
- @repo_id = params[:repo_id]
- repo = client.repo(@repo_id)
- @target_namespace = params[:new_namespace].presence || repo.namespace
- @project_name = repo.name
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute
- end
-
- private
-
- def client
- @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos])
- end
-
- def verify_gitorious_import_enabled
- render_404 unless gitorious_import_enabled?
- end
-end
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
new file mode 100644
index 00000000000..f3759b4c0ea
--- /dev/null
+++ b/app/controllers/koding_controller.rb
@@ -0,0 +1,15 @@
+class KodingController < ApplicationController
+ before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ layout 'koding'
+
+ def index
+ path = File.join(Rails.root, 'doc/user/project/koding.md')
+ @markdown = File.read(path)
+ end
+
+ private
+
+ def check_integration!
+ render_404 unless current_application_settings.koding_enabled?
+ end
+end
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index f35d631df0c..f54c79c2e37 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -107,7 +107,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Only allow properly saved users to login.
if @user.persisted? && @user.valid?
log_audit_event(@user, with: oauth['provider'])
- sign_in_and_redirect(@user)
+ if @user.two_factor_enabled?
+ prompt_for_two_factor(@user)
+ else
+ sign_in_and_redirect(@user)
+ end
else
error_message = @user.errors.full_messages.to_sentence
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index c780e0983f9..6217ec5ecef 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -50,6 +50,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
flash[:notice] = "Password was successfully updated. Please login with it"
redirect_to new_user_session_path
else
+ @user.reload
render 'edit'
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 6a358fdcc05..9eb75bb3891 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -43,11 +43,11 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# A U2F (universal 2nd factor) device's information is stored after successful
# registration, which is then used while 2FA authentication is taking place.
def create_u2f
- @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, params[:device_response], session[:challenges])
+ @u2f_registration = U2fRegistration.register(current_user, u2f_app_id, u2f_registration_params, session[:challenges])
if @u2f_registration.persisted?
session.delete(:challenges)
- redirect_to profile_account_path, notice: "Your U2F device was registered!"
+ redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!"
else
@qr_code = build_qr_code
setup_u2f_registration
@@ -91,16 +91,19 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
# Actual communication is performed using a Javascript API
def setup_u2f_registration
@u2f_registration ||= U2fRegistration.new
- @registration_key_handles = current_user.u2f_registrations.pluck(:key_handle)
+ @u2f_registrations = current_user.u2f_registrations
u2f = U2F::U2F.new(u2f_app_id)
registration_requests = u2f.registration_requests
- sign_requests = u2f.authentication_requests(@registration_key_handles)
+ sign_requests = u2f.authentication_requests(@u2f_registrations.map(&:key_handle))
session[:challenges] = registration_requests.map(&:challenge)
gon.push(u2f: { challenges: session[:challenges], app_id: u2f_app_id,
register_requests: registration_requests,
- sign_requests: sign_requests,
- browser_supports_u2f: browser_supports_u2f? })
+ sign_requests: sign_requests })
+ end
+
+ def u2f_registration_params
+ params.require(:u2f_registration).permit(:device_response, :name)
end
end
diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb
new file mode 100644
index 00000000000..c02fe85c3cc
--- /dev/null
+++ b/app/controllers/profiles/u2f_registrations_controller.rb
@@ -0,0 +1,7 @@
+class Profiles::U2fRegistrationsController < Profiles::ApplicationController
+ def destroy
+ u2f_registration = current_user.u2f_registrations.find(params[:id])
+ u2f_registration.destroy
+ redirect_to profile_two_factor_auth_path, notice: "Successfully deleted U2F device."
+ end
+end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 996909a28c6..91315a07deb 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -83,6 +83,7 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
+ @show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index f11c8321464..7241949393b 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -23,10 +23,9 @@ class Projects::ArtifactsController < Projects::ApplicationController
entry = build.artifacts_metadata_entry(params[:path])
if entry.exists?
- render json: { archive: build.artifacts_file.path,
- entry: Base64.encode64(entry.path) }
+ send_artifacts_entry(build, entry)
else
- render json: {}, status: 404
+ render_404
end
end
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 5962f74c39b..ada7db3c552 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -4,7 +4,7 @@ class Projects::AvatarsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def show
- @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
+ @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 824aa41db51..6c25cd83a24 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -3,18 +3,27 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:index]
before_action :no_cache_headers, except: [:index]
- def index
- @ref = params[:ref] || @project.default_branch || 'master'
- @build_badge = Gitlab::Badge::Build.new(@project, @ref)
+ def build
+ build_status = Gitlab::Badge::Build::Status
+ .new(project, params[:ref])
+
+ render_badge build_status
end
- def build
- badge = Gitlab::Badge::Build.new(project, params[:ref])
+ def coverage
+ coverage_report = Gitlab::Badge::Coverage::Report
+ .new(project, params[:ref], params[:job])
+
+ render_badge coverage_report
+ end
+
+ private
+ def render_badge(badge)
respond_to do |format|
format.html { render_404 }
format.svg do
- send_data(badge.data, type: badge.type, disposition: 'inline')
+ render 'badge', locals: { badge: badge.template }
end
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 5356fdf010d..cdf9a04bacf 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -17,6 +17,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_branch_head, only: [:edit, :update]
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
+ before_action :set_last_commit_sha, only: [:edit, :update]
def new
commit unless @repository.empty?
@@ -33,11 +34,16 @@ class Projects::BlobController < Projects::ApplicationController
end
def edit
- @last_commit = Gitlab::Git::Commit.last_for_path(@repository, @ref, @path).sha
blob.load_all_data!(@repository)
end
def update
+ if params[:file_path].present?
+ @previous_path = @path
+ @path = params[:file_path]
+ @commit_params[:file_path] = @path
+ end
+
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) +
@@ -49,6 +55,10 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: after_edit_path,
failure_view: :edit,
failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
+
+ rescue Files::UpdateService::FileChangedError
+ @conflict = true
+ render :edit
end
def preview
@@ -70,6 +80,8 @@ class Projects::BlobController < Projects::ApplicationController
end
def diff
+ apply_diff_view_cookie!
+
@form = UnfoldForm.new(params)
@lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path)
@lines = @lines[@form.since - 1..@form.to - 1]
@@ -144,7 +156,8 @@ class Projects::BlobController < Projects::ApplicationController
file_path: @file_path,
commit_message: params[:commit_message],
file_content: params[:content],
- file_content_encoding: params[:encoding]
+ file_content_encoding: params[:encoding],
+ last_commit_sha: params[:last_commit_sha]
}
end
@@ -153,4 +166,9 @@ class Projects::BlobController < Projects::ApplicationController
render nothing: true
end
end
+
+ def set_last_commit_sha
+ @last_commit_sha = Gitlab::Git::Commit.
+ last_for_path(@repository, @ref, @path).sha
+ end
end
diff --git a/app/controllers/projects/board_lists_controller.rb b/app/controllers/projects/board_lists_controller.rb
new file mode 100644
index 00000000000..3cfb08d5822
--- /dev/null
+++ b/app/controllers/projects/board_lists_controller.rb
@@ -0,0 +1,65 @@
+class Projects::BoardListsController < Projects::ApplicationController
+ respond_to :json
+
+ before_action :authorize_admin_list!
+
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+ def create
+ list = Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+ if list.valid?
+ render json: list.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+ else
+ render json: list.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ service = Boards::Lists::MoveService.new(project, current_user, move_params)
+
+ if service.execute
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def destroy
+ service = Boards::Lists::DestroyService.new(project, current_user, params)
+
+ if service.execute
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def generate
+ service = Boards::Lists::GenerateService.new(project, current_user)
+
+ if service.execute
+ render json: project.board.lists.label.as_json(only: [:id, :list_type, :position], methods: [:title], include: { label: { only: [:id, :title, :description, :color, :priority] } })
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def authorize_admin_list!
+ return render_403 unless can?(current_user, :admin_list, project)
+ end
+
+ def list_params
+ params.require(:list).permit(:label_id)
+ end
+
+ def move_params
+ params.require(:list).permit(:position).merge(id: params[:id])
+ end
+
+ def record_not_found(exception)
+ render json: { error: exception.message }, status: :not_found
+ end
+end
diff --git a/app/controllers/projects/boards/application_controller.rb b/app/controllers/projects/boards/application_controller.rb
new file mode 100644
index 00000000000..dad38fff6b9
--- /dev/null
+++ b/app/controllers/projects/boards/application_controller.rb
@@ -0,0 +1,15 @@
+module Projects
+ module Boards
+ class ApplicationController < Projects::ApplicationController
+ respond_to :json
+
+ rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
+
+ private
+
+ def record_not_found(exception)
+ render json: { error: exception.message }, status: :not_found
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
new file mode 100644
index 00000000000..1a4f6b50e8f
--- /dev/null
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -0,0 +1,56 @@
+module Projects
+ module Boards
+ class IssuesController < Boards::ApplicationController
+ before_action :authorize_read_issue!, only: [:index]
+ before_action :authorize_update_issue!, only: [:update]
+
+ def index
+ issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
+ issues = issues.page(params[:page])
+
+ render json: issues.as_json(
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ })
+ end
+
+ def update
+ service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
+
+ if service.execute(issue)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def issue
+ @issue ||=
+ IssuesFinder.new(current_user, project_id: project.id, state: 'all')
+ .execute
+ .where(iid: params[:id])
+ .first!
+ end
+
+ def authorize_read_issue!
+ return render_403 unless can?(current_user, :read_issue, project)
+ end
+
+ def authorize_update_issue!
+ return render_403 unless can?(current_user, :update_issue, issue)
+ end
+
+ def filter_params
+ params.merge(id: params[:list_id])
+ end
+
+ def move_params
+ params.permit(:id, :from_list_id, :to_list_id)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb
new file mode 100644
index 00000000000..b995f586737
--- /dev/null
+++ b/app/controllers/projects/boards/lists_controller.rb
@@ -0,0 +1,81 @@
+module Projects
+ module Boards
+ class ListsController < Boards::ApplicationController
+ before_action :authorize_admin_list!, only: [:create, :update, :destroy, :generate]
+ before_action :authorize_read_list!, only: [:index]
+
+ def index
+ render json: serialize_as_json(project.board.lists)
+ end
+
+ def create
+ list = ::Boards::Lists::CreateService.new(project, current_user, list_params).execute
+
+ if list.valid?
+ render json: serialize_as_json(list)
+ else
+ render json: list.errors, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ list = project.board.lists.movable.find(params[:id])
+ service = ::Boards::Lists::MoveService.new(project, current_user, move_params)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def destroy
+ list = project.board.lists.destroyable.find(params[:id])
+ service = ::Boards::Lists::DestroyService.new(project, current_user, params)
+
+ if service.execute(list)
+ head :ok
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ def generate
+ service = ::Boards::Lists::GenerateService.new(project, current_user)
+
+ if service.execute
+ render json: serialize_as_json(project.board.lists.movable)
+ else
+ head :unprocessable_entity
+ end
+ end
+
+ private
+
+ def authorize_admin_list!
+ return render_403 unless can?(current_user, :admin_list, project)
+ end
+
+ def authorize_read_list!
+ return render_403 unless can?(current_user, :read_list, project)
+ end
+
+ def list_params
+ params.require(:list).permit(:label_id)
+ end
+
+ def move_params
+ params.require(:list).permit(:position)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ only: [:id, :list_type, :position],
+ methods: [:title],
+ include: {
+ label: { only: [:id, :title, :description, :color, :priority] }
+ })
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
new file mode 100644
index 00000000000..33206717089
--- /dev/null
+++ b/app/controllers/projects/boards_controller.rb
@@ -0,0 +1,15 @@
+class Projects::BoardsController < Projects::ApplicationController
+ respond_to :html
+
+ before_action :authorize_read_board!, only: [:show]
+
+ def show
+ ::Boards::CreateService.new(project, current_user).execute
+ end
+
+ private
+
+ def authorize_read_board!
+ return access_denied! unless can?(current_user, :read_board, project)
+ end
+end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index dd9508da049..2de8ada3e29 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,19 +1,27 @@
class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
+ include SortingHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy]
def index
- @sort = params[:sort] || 'name'
- @branches = @repository.branches_sorted_by(@sort)
+ @sort = params[:sort].presence || sort_value_name
+ @branches = BranchesFinder.new(@repository, params).execute
@branches = Kaminari.paginate_array(@branches).page(params[:page])
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @repository.branch_names
+ end
+ end
end
def recent
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index ef3051d7519..12195c3cbb8 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,17 +1,19 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
+ before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry, :play]
before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project'
def index
@scope = params[:scope]
- @all_builds = project.builds
+ @all_builds = project.builds.relevant
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
+ when 'pending'
+ @builds.pending.reverse_order
when 'running'
- @builds.running_or_pending.reverse_order
+ @builds.running.reverse_order
when 'finished'
@builds.finished
else
@@ -47,14 +49,19 @@ class Projects::BuildsController < Projects::ApplicationController
end
def retry
- unless @build.retryable?
- return render_404
- end
+ return render_404 unless @build.retryable?
build = Ci::Build.retry(@build, current_user)
redirect_to build_path(build)
end
+ def play
+ return render_404 unless @build.playable?
+
+ build = @build.play(current_user)
+ redirect_to build_path(build)
+ end
+
def cancel
@build.cancel
redirect_to build_path(@build)
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 37d6521026c..02fb3f56890 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -3,6 +3,7 @@
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
include CreatesCommit
+ include DiffForPath
include DiffHelper
# Authorize
@@ -11,29 +12,14 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_show_vars, only: [:show, :builds]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
+ before_action :define_status_vars, only: [:show, :builds]
+ before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show
apply_diff_view_cookie!
- @grouped_diff_notes = commit.notes.grouped_diff_notes
- @notes = commit.notes.non_diff_notes.fresh
-
- Banzai::NoteRenderer.render(
- @grouped_diff_notes.values.flatten + @notes,
- @project,
- current_user,
- )
-
- @note = @project.build_commit_note(commit)
-
- @noteable = @commit
- @comments_target = {
- noteable_type: 'Commit',
- commit_id: @commit.id
- }
-
respond_to do |format|
format.html
format.diff { render text: @commit.to_diff }
@@ -41,6 +27,10 @@ class Projects::CommitController < Projects::ApplicationController
end
end
+ def diff_for_path
+ render_diff_for_path(@commit.diffs(diff_options))
+ end
+
def builds
end
@@ -103,7 +93,7 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
- @commit ||= @project.commit(params[:id])
+ @noteable = @commit ||= @project.commit(params[:id])
end
def pipelines
@@ -114,7 +104,7 @@ class Projects::CommitController < Projects::ApplicationController
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
- def define_show_vars
+ def define_commit_vars
return git_not_found! unless commit
opts = diff_options
@@ -122,9 +112,30 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts)
@notes_count = commit.notes.count
+ end
+
+ def define_note_vars
+ @grouped_diff_discussions = commit.notes.grouped_diff_discussions
+ @notes = commit.notes.non_diff_notes.fresh
+
+ Banzai::NoteRenderer.render(
+ @grouped_diff_discussions.values.flat_map(&:notes) + @notes,
+ @project,
+ current_user,
+ )
+
+ @note = @project.build_commit_note(commit)
+
+ @noteable = @commit
+ @comments_target = {
+ noteable_type: 'Commit',
+ commit_id: @commit.id
+ }
+ end
- @statuses = CommitStatus.where(pipeline: pipelines)
- @builds = Ci::Build.where(pipeline: pipelines)
+ def define_status_vars
+ @statuses = CommitStatus.where(pipeline: pipelines).relevant
+ @builds = Ci::Build.where(pipeline: pipelines).relevant
end
def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d240b9fe989..bee3d56076c 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,38 +1,27 @@
require 'addressable/uri'
class Projects::CompareController < Projects::ApplicationController
+ include DiffForPath
include DiffHelper
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :assign_ref_vars, only: [:index, :show]
+ before_action :define_ref_vars, only: [:index, :show, :diff_for_path]
+ before_action :define_diff_vars, only: [:show, :diff_for_path]
before_action :merge_request, only: [:index, :show]
def index
end
def show
- compare = CompareService.new.
- execute(@project, @head_ref, @project, @start_ref, diff_options)
-
- if compare
- @commits = Commit.decorate(compare.commits, @project)
-
- @start_commit = @project.commit(@start_ref)
- @commit = @project.commit(@head_ref)
- @base_commit = @project.merge_base_commit(@start_ref, @head_ref)
+ apply_diff_view_cookie!
+ end
- @diffs = compare.diffs(diff_options)
- @diff_refs = Gitlab::Diff::DiffRefs.new(
- base_sha: @base_commit.try(:sha),
- start_sha: @start_commit.try(:sha),
- head_sha: @commit.try(:sha)
- )
+ def diff_for_path
+ return render_404 unless @compare
- @diff_notes_disabled = true
- @grouped_diff_notes = {}
- end
+ render_diff_for_path(@compare.diffs(diff_options))
end
def create
@@ -42,11 +31,27 @@ class Projects::CompareController < Projects::ApplicationController
private
- def assign_ref_vars
+ def define_ref_vars
@start_ref = Addressable::URI.unescape(params[:from])
@ref = @head_ref = Addressable::URI.unescape(params[:to])
end
+ def define_diff_vars
+ @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref)
+
+ if @compare
+ @commits = @compare.commits
+ @start_commit = @compare.start_commit
+ @commit = @compare.commit
+ @base_commit = @compare.base_commit
+
+ @diffs = @compare.diffs(diff_options)
+
+ @diff_notes_disabled = true
+ @grouped_diff_discussions = {}
+ end
+ end
+
def merge_request
@merge_request ||= @project.merge_requests.opened.
find_by(source_project: @project, source_branch: @head_ref, target_branch: @start_ref)
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 83d5ced9be8..529e0aa2d33 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -12,8 +12,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def new
- redirect_to namespace_project_deploy_keys_path(@project.namespace,
- @project)
+ redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
end
def create
@@ -21,19 +20,16 @@ class Projects::DeployKeysController < Projects::ApplicationController
set_index_vars
if @key.valid? && @project.deploy_keys << @key
- redirect_to namespace_project_deploy_keys_path(@project.namespace,
- @project)
+ redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
else
render "index"
end
end
def enable
- @key = accessible_keys.find(params[:id])
- @project.deploy_keys << @key
+ Projects::EnableDeployKeyService.new(@project, current_user, params).execute
- redirect_to namespace_project_deploy_keys_path(@project.namespace,
- @project)
+ redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
end
def disable
@@ -45,9 +41,9 @@ class Projects::DeployKeysController < Projects::ApplicationController
protected
def set_index_vars
- @enabled_keys ||= @project.deploy_keys
+ @enabled_keys ||= @project.deploy_keys
- @available_keys ||= accessible_keys - @enabled_keys
+ @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys
@available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
@available_public_keys ||= DeployKey.are_public - @enabled_keys
@@ -56,10 +52,6 @@ class Projects::DeployKeysController < Projects::ApplicationController
@available_public_keys -= @available_project_keys
end
- def accessible_keys
- @accessible_keys ||= current_user.accessible_deploy_keys
- end
-
def deploy_key_params
params.require(:deploy_key).permit(:key, :title)
end
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
new file mode 100644
index 00000000000..b2e8733ccb7
--- /dev/null
+++ b/app/controllers/projects/discussions_controller.rb
@@ -0,0 +1,43 @@
+class Projects::DiscussionsController < Projects::ApplicationController
+ before_action :module_enabled
+ before_action :merge_request
+ before_action :discussion
+ before_action :authorize_resolve_discussion!
+
+ def resolve
+ discussion.resolve!(current_user)
+
+ MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(merge_request)
+
+ render json: {
+ resolved_by: discussion.resolved_by.try(:name),
+ discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
+ }
+ end
+
+ def unresolve
+ discussion.unresolve!
+
+ render json: {
+ discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
+ }
+ end
+
+ private
+
+ def merge_request
+ @merge_request ||= @project.merge_requests.find_by!(iid: params[:merge_request_id])
+ end
+
+ def discussion
+ @discussion ||= @merge_request.find_diff_discussion(params[:id]) || render_404
+ end
+
+ def authorize_resolve_discussion!
+ access_denied! unless discussion.can_resolve?(current_user)
+ end
+
+ def module_enabled
+ render_404 unless @project.merge_requests_enabled
+ end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4b433796161..58678f96879 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -2,8 +2,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
layout 'project'
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
- before_action :authorize_update_environment!, only: [:destroy]
- before_action :environment, only: [:show, :destroy]
+ before_action :authorize_update_environment!, only: [:edit, :update, :destroy]
+ before_action :environment, only: [:show, :edit, :update, :destroy]
def index
@environments = project.environments
@@ -17,13 +17,24 @@ class Projects::EnvironmentsController < Projects::ApplicationController
@environment = project.environments.new
end
+ def edit
+ end
+
def create
- @environment = project.environments.create(create_params)
+ @environment = project.environments.create(environment_params)
if @environment.persisted?
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
else
- render 'new'
+ render :new
+ end
+ end
+
+ def update
+ if @environment.update(environment_params)
+ redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ else
+ render :edit
end
end
@@ -39,8 +50,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
private
- def create_params
- params.require(:environment).permit(:name)
+ def environment_params
+ params.require(:environment).permit(:name, :external_url)
end
def environment
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
new file mode 100644
index 00000000000..a5b4031c30f
--- /dev/null
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -0,0 +1,120 @@
+# This file should be identical in GitLab Community Edition and Enterprise Edition
+
+class Projects::GitHttpClientController < Projects::ApplicationController
+ include ActionController::HttpAuthentication::Basic
+ include KerberosSpnegoHelper
+
+ attr_reader :user
+
+ # Git clients will not know what authenticity token to send along
+ skip_before_action :verify_authenticity_token
+ skip_before_action :repository
+ before_action :authenticate_user
+ before_action :ensure_project_found!
+
+ private
+
+ def authenticate_user
+ if project && project.public? && download_request?
+ return # Allow access
+ end
+
+ if allow_basic_auth? && basic_auth_provided?
+ login, password = user_name_and_password(request)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+
+ if auth_result.type == :ci && download_request?
+ @ci = true
+ elsif auth_result.type == :oauth && !download_request?
+ # Not allowed
+ elsif auth_result.type == :missing_personal_token
+ render_missing_personal_token
+ return # Render above denied access, nothing left to do
+ else
+ @user = auth_result.user
+ end
+
+ if ci? || user
+ return # Allow access
+ end
+ elsif allow_kerberos_spnego_auth? && spnego_provided?
+ @user = find_kerberos_user
+
+ if user
+ send_final_spnego_response
+ return # Allow access
+ end
+ end
+
+ send_challenges
+ render plain: "HTTP Basic: Access denied\n", status: 401
+ end
+
+ def basic_auth_provided?
+ has_basic_credentials?(request)
+ end
+
+ def send_challenges
+ challenges = []
+ challenges << 'Basic realm="GitLab"' if allow_basic_auth?
+ challenges << spnego_challenge if allow_kerberos_spnego_auth?
+ headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
+ end
+
+ def ensure_project_found!
+ render_not_found if project.blank?
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ project_id, _ = project_id_with_suffix
+ if project_id.blank?
+ @project = nil
+ else
+ @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
+ end
+ end
+
+ # This method returns two values so that we can parse
+ # params[:project_id] (untrusted input!) in exactly one place.
+ def project_id_with_suffix
+ id = params[:project_id] || ''
+
+ %w[.wiki.git .git].each do |suffix|
+ if id.end_with?(suffix)
+ # Be careful to only remove the suffix from the end of 'id'.
+ # Accidentally removing it from the middle is how security
+ # vulnerabilities happen!
+ return [id.slice(0, id.length - suffix.length), suffix]
+ end
+ end
+
+ # Something is wrong with params[:project_id]; do not pass it on.
+ [nil, nil]
+ end
+
+ def render_missing_personal_token
+ render plain: "HTTP Basic: Access denied\n" \
+ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You can generate one at #{profile_personal_access_tokens_url}",
+ status: 401
+ end
+
+ def repository
+ _, suffix = project_id_with_suffix
+ if suffix == '.wiki.git'
+ project.wiki.repository
+ else
+ project.repository
+ end
+ end
+
+ def render_not_found
+ render plain: 'Not Found', status: :not_found
+ end
+
+ def ci?
+ @ci.present?
+ end
+end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 40a8b7940d9..b4373ef89ef 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,17 +1,6 @@
# This file should be identical in GitLab Community Edition and Enterprise Edition
-class Projects::GitHttpController < Projects::ApplicationController
- include ActionController::HttpAuthentication::Basic
- include KerberosSpnegoHelper
-
- attr_reader :user
-
- # Git clients will not know what authenticity token to send along
- skip_before_action :verify_authenticity_token
- skip_before_action :repository
- before_action :authenticate_user
- before_action :ensure_project_found!
-
+class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
@@ -20,9 +9,9 @@ class Projects::GitHttpController < Projects::ApplicationController
elsif receive_pack? && receive_pack_allowed?
render_ok
elsif http_blocked?
- render_not_allowed
+ render_http_not_allowed
else
- render_not_found
+ render_denied
end
end
@@ -31,7 +20,7 @@ class Projects::GitHttpController < Projects::ApplicationController
if upload_pack? && upload_pack_allowed?
render_ok
else
- render_not_found
+ render_denied
end
end
@@ -40,87 +29,14 @@ class Projects::GitHttpController < Projects::ApplicationController
if receive_pack? && receive_pack_allowed?
render_ok
else
- render_not_found
+ render_denied
end
end
private
- def authenticate_user
- if project && project.public? && upload_pack?
- return # Allow access
- end
-
- if allow_basic_auth? && basic_auth_provided?
- login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
- if auth_result.type == :ci && upload_pack?
- @ci = true
- elsif auth_result.type == :oauth && !upload_pack?
- # Not allowed
- else
- @user = auth_result.user
- end
-
- if ci? || user
- return # Allow access
- end
- elsif allow_kerberos_spnego_auth? && spnego_provided?
- @user = find_kerberos_user
-
- if user
- send_final_spnego_response
- return # Allow access
- end
- end
-
- send_challenges
- render plain: "HTTP Basic: Access denied\n", status: 401
- end
-
- def basic_auth_provided?
- has_basic_credentials?(request)
- end
-
- def send_challenges
- challenges = []
- challenges << 'Basic realm="GitLab"' if allow_basic_auth?
- challenges << spnego_challenge if allow_kerberos_spnego_auth?
- headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
- end
-
- def ensure_project_found!
- render_not_found if project.blank?
- end
-
- def project
- return @project if defined?(@project)
-
- project_id, _ = project_id_with_suffix
- if project_id.blank?
- @project = nil
- else
- @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}")
- end
- end
-
- # This method returns two values so that we can parse
- # params[:project_id] (untrusted input!) in exactly one place.
- def project_id_with_suffix
- id = params[:project_id] || ''
-
- %w[.wiki.git .git].each do |suffix|
- if id.end_with?(suffix)
- # Be careful to only remove the suffix from the end of 'id'.
- # Accidentally removing it from the middle is how security
- # vulnerabilities happen!
- return [id.slice(0, id.length - suffix.length), suffix]
- end
- end
-
- # Something is wrong with params[:project_id]; do not pass it on.
- [nil, nil]
+ def download_request?
+ upload_pack?
end
def upload_pack?
@@ -143,47 +59,37 @@ class Projects::GitHttpController < Projects::ApplicationController
render json: Gitlab::Workhorse.git_http_ok(repository, user)
end
- def repository
- _, suffix = project_id_with_suffix
- if suffix == '.wiki.git'
- project.wiki.repository
- else
- project.repository
- end
- end
-
- def render_not_found
- render plain: 'Not Found', status: :not_found
+ def render_http_not_allowed
+ render plain: access_check.message, status: :forbidden
end
- def render_not_allowed
- render plain: download_access.message, status: :forbidden
- end
-
- def ci?
- @ci.present?
+ def render_denied
+ if user && user.can?(:read_project, project)
+ render plain: 'Access denied', status: :forbidden
+ else
+ # Do not leak information about project existence
+ render_not_found
+ end
end
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
if user
- download_access.allowed?
+ access_check.allowed?
else
ci? || project.public?
end
end
def access
- return @access if defined?(@access)
-
- @access = Gitlab::GitAccess.new(user, project, 'http')
+ @access ||= Gitlab::GitAccess.new(user, project, 'http')
end
- def download_access
- return @download_access if defined?(@download_access)
-
- @download_access = access.check('git-upload-pack')
+ def access_check
+ # Use the magic string '_any' to indicate we do not know what the
+ # changes are. This is also what gitlab-shell does.
+ @access_check ||= access.check(git_command, '_any')
end
def http_blocked?
@@ -193,8 +99,6 @@ class Projects::GitHttpController < Projects::ApplicationController
def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack
- # Skip user authorization on upload request.
- # It will be done by the pre-receive hook in the repository.
- user.present?
+ access_check.allowed?
end
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa853..d0c4550733c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
- group: group, group_access: params[:link_group_access]
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index a60027ff477..b5624046387 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(
:build_events,
+ :pipeline_events,
:enable_ssl_verification,
:issues_events,
:merge_requests_events,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b6e80762e3c..7c03dcd2e64 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,8 +1,12 @@
class Projects::IssuesController < Projects::ApplicationController
+ include NotesHelper
include ToggleSubscriptionAction
include IssuableActions
include ToggleAwardEmoji
+ include IssuableCollections
+ include SpammableActions
+ before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch]
@@ -23,7 +27,7 @@ class Projects::IssuesController < Projects::ApplicationController
def index
terms = params['issue_search']
- @issues = get_issues_collection
+ @issues = issues_collection
if terms.present?
if terms =~ /\A#(\d+)\z/
@@ -70,6 +74,8 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@noteable = @issue
+ preload_max_access_for_authors(@notes, @project)
+
respond_to do |format|
format.html
format.json do
@@ -79,7 +85,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def create
- @issue = Issues::CreateService.new(project, current_user, issue_params).execute
+ @issue = Issues::CreateService.new(project, current_user, issue_params.merge(request: request)).execute
respond_to do |format|
format.html do
@@ -89,7 +95,7 @@ class Projects::IssuesController < Projects::ApplicationController
render :new
end
end
- format.js do |format|
+ format.js do
@link = @issue.attachment.url.to_js
end
end
@@ -119,6 +125,10 @@ class Projects::IssuesController < Projects::ApplicationController
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
end
end
+
+ rescue ActiveRecord::StaleObjectError
+ @conflict = true
+ render :edit
end
def referenced_merge_requests
@@ -171,15 +181,12 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
- @issue ||= begin
- @project.issues.find_by!(iid: params[:id])
- rescue ActiveRecord::RecordNotFound
- redirect_old
- end
+ @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
+ alias_method :spammable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
@@ -197,6 +204,18 @@ class Projects::IssuesController < Projects::ApplicationController
return render_404 unless @project.issues_enabled && @project.default_issues_tracker?
end
+ def redirect_to_external_issue_tracker
+ external = @project.external_issue_tracker
+
+ return unless external
+
+ if action_name == 'new'
+ redirect_to external.new_issue_path
+ else
+ redirect_to external.project_path
+ end
+ end
+
# Since iids are implemented only in 6.1
# user may navigate to issue page using old global ids.
#
@@ -207,7 +226,6 @@ class Projects::IssuesController < Projects::ApplicationController
if issue
redirect_to issue_path(issue)
- return
else
raise ActiveRecord::RecordNotFound.new
end
@@ -216,7 +234,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :due_date, :state_event, :task_num, label_ids: []
+ :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
@@ -226,6 +244,7 @@ class Projects::IssuesController < Projects::ApplicationController
:assignee_id,
:milestone_id,
:state_event,
+ :subscription_event,
label_ids: [],
add_label_ids: [],
remove_label_ids: []
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
new file mode 100644
index 00000000000..ece49dcd922
--- /dev/null
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -0,0 +1,94 @@
+class Projects::LfsApiController < Projects::GitHttpClientController
+ include LfsHelper
+
+ before_action :require_lfs_enabled!
+ before_action :lfs_check_access!, except: [:deprecated]
+
+ def batch
+ unless objects.present?
+ render_lfs_not_found
+ return
+ end
+
+ if download_request?
+ render json: { objects: download_objects! }
+ elsif upload_request?
+ render json: { objects: upload_objects! }
+ else
+ raise "Never reached"
+ end
+ end
+
+ def deprecated
+ render(
+ json: {
+ message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
+ documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ },
+ status: 501
+ )
+ end
+
+ private
+
+ def objects
+ @objects ||= (params[:objects] || []).to_a
+ end
+
+ def existing_oids
+ @existing_oids ||= begin
+ storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+ end
+ end
+
+ def download_objects!
+ objects.each do |object|
+ if existing_oids.include?(object[:oid])
+ object[:actions] = download_actions(object)
+ else
+ object[:error] = {
+ code: 404,
+ message: "Object does not exist on the server or you don't have permissions to access it",
+ }
+ end
+ end
+ objects
+ end
+
+ def upload_objects!
+ objects.each do |object|
+ object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid])
+ end
+ objects
+ end
+
+ def download_actions(object)
+ {
+ download: {
+ href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
+ header: {
+ Authorization: request.headers['Authorization']
+ }.compact
+ }
+ }
+ end
+
+ def upload_actions(object)
+ {
+ upload: {
+ href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
+ header: {
+ Authorization: request.headers['Authorization']
+ }.compact
+ }
+ }
+ 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
new file mode 100644
index 00000000000..69066cb40e6
--- /dev/null
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -0,0 +1,92 @@
+class Projects::LfsStorageController < Projects::GitHttpClientController
+ include LfsHelper
+
+ before_action :require_lfs_enabled!
+ before_action :lfs_check_access!
+
+ def download
+ lfs_object = LfsObject.find_by_oid(oid)
+ unless lfs_object && lfs_object.file.exists?
+ render_lfs_not_found
+ return
+ end
+
+ send_file lfs_object.file.path, content_type: "application/octet-stream"
+ end
+
+ def upload_authorize
+ render(
+ json: {
+ StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ LfsOid: oid,
+ LfsSize: size,
+ },
+ content_type: 'application/json; charset=utf-8'
+ )
+ end
+
+ def upload_finalize
+ unless tmp_filename
+ render_lfs_forbidden
+ return
+ end
+
+ if store_file(oid, size, tmp_filename)
+ head 200
+ else
+ render plain: 'Unprocessable entity', status: 422
+ end
+ end
+
+ private
+
+ def download_request?
+ action_name == 'download'
+ end
+
+ def upload_request?
+ %w[upload_authorize upload_finalize].include? action_name
+ end
+
+ def oid
+ params[:oid].to_s
+ end
+
+ def size
+ params[:size].to_i
+ end
+
+ def tmp_filename
+ name = request.headers['X-Gitlab-Lfs-Tmp']
+ return if name.include?('/')
+ return unless oid.present? && name.start_with?(oid)
+ name
+ end
+
+ def store_file(oid, size, tmp_file)
+ # Define tmp_file_path early because we use it in "ensure"
+ tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+
+ object = LfsObject.find_or_create_by(oid: oid, size: size)
+ file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path)
+ file_exists && link_to_project(object)
+ ensure
+ FileUtils.rm_f(tmp_file_path)
+ end
+
+ def move_tmp_file_to_storage(object, path)
+ File.open(path) do |f|
+ object.file = f
+ end
+
+ object.file.store!
+ object.save
+ end
+
+ def link_to_project(object)
+ if object && !object.projects.exists?(storage_project.id)
+ object.projects << storage_project
+ object.save
+ end
+ end
+end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 5678a4015b6..4f5f3b6aa09 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,18 +1,23 @@
class Projects::MergeRequestsController < Projects::ApplicationController
include ToggleSubscriptionAction
+ include DiffForPath
include DiffHelper
include IssuableActions
+ include NotesHelper
include ToggleAwardEmoji
+ include IssuableCollections
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
- :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
+ :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check,
+ :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts
]
- before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
- before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
- before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
+ before_action :define_commit_vars, only: [:diffs]
+ before_action :define_diff_comment_vars, only: [:diffs]
+ before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -23,9 +28,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
# Allow modify merge_request
before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
+ before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts]
+
def index
terms = params['issue_search']
- @merge_requests = get_merge_requests_collection
+ @merge_requests = merge_requests_collection
if terms.present?
if terms =~ /\A[#!](\d+)\z/
@@ -53,8 +60,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def show
respond_to do |format|
- format.html
-
+ format.html { define_discussion_vars }
+
format.json do
render json: @merge_request
end
@@ -76,37 +83,53 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
- @merge_request_diff = @merge_request.merge_request_diff
-
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
- @comments_target = {
- noteable_type: 'MergeRequest',
- noteable_id: @merge_request.id
- }
+ respond_to do |format|
+ format.html { define_discussion_vars }
+ format.json do
+ unless @merge_request_diff.latest?
+ # Disable comments if browsing older version of the diff
+ @diff_notes_disabled = true
+ end
- @use_legacy_diff_notes = !@merge_request.support_new_diff_notes?
- @grouped_diff_notes = @merge_request.notes.grouped_diff_notes
+ @diffs = @merge_request_diff.diffs(diff_options)
- Banzai::NoteRenderer.render(
- @grouped_diff_notes.values.flatten,
- @project,
- current_user,
- @path,
- @project_wiki,
- @ref
- )
+ render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
+ end
+ end
+ end
- respond_to do |format|
- format.html
- format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
+ # With an ID param, loads the MR at that ID. Otherwise, accepts the same params as #new
+ # and uses that (unsaved) MR.
+ #
+ def diff_for_path
+ if params[:id]
+ merge_request
+ define_diff_comment_vars
+ else
+ build_merge_request
+ @diff_notes_disabled = true
+ @grouped_diff_discussions = {}
end
+
+ define_commit_vars
+
+ render_diff_for_path(@merge_request.diffs(diff_options))
end
def commits
respond_to do |format|
- format.html { render 'show' }
+ format.html do
+ define_discussion_vars
+
+ render 'show'
+ end
format.json do
# Get commits from repository
# or from cache if already merged
@@ -119,16 +142,75 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def conflicts
+ respond_to do |format|
+ format.html { define_discussion_vars }
+
+ format.json do
+ if @merge_request.conflicts_can_be_resolved_in_ui?
+ render json: @merge_request.conflicts
+ elsif @merge_request.can_be_merged?
+ render json: {
+ message: 'The merge conflicts for this merge request have already been resolved. Please return to the merge request.',
+ type: 'error'
+ }
+ else
+ render json: {
+ message: 'The merge conflicts for this merge request cannot be resolved through GitLab. Please try to resolve them locally.',
+ type: 'error'
+ }
+ end
+ end
+ end
+ end
+
+ def resolve_conflicts
+ return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui?
+
+ if @merge_request.can_be_merged?
+ render status: :bad_request, json: { message: 'The merge conflicts for this merge request have already been resolved.' }
+ return
+ end
+
+ begin
+ MergeRequests::ResolveService.new(@merge_request.source_project, current_user, params).execute(@merge_request)
+
+ flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
+
+ render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) }
+ rescue Gitlab::Conflict::File::MissingResolution => e
+ render status: :bad_request, json: { message: e.message }
+ end
+ end
+
def builds
respond_to do |format|
- format.html { render 'show' }
+ format.html do
+ define_discussion_vars
+
+ render 'show'
+ end
format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
end
end
+ def pipelines
+ @pipelines = @merge_request.all_pipelines
+
+ respond_to do |format|
+ format.html do
+ define_discussion_vars
+
+ render 'show'
+ end
+ format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } }
+ end
+ end
+
def new
- params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ apply_diff_view_cookie!
+
+ build_merge_request
@noteable = @merge_request
@target_branches = if @merge_request.target_project
@@ -142,11 +224,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit
- @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
+ @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
@diff_notes_disabled = true
-
@pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses if @pipeline
+ @statuses = @pipeline.statuses.relevant if @pipeline
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
@@ -187,6 +268,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
render "edit"
end
+ rescue ActiveRecord::StaleObjectError
+ @conflict = true
+ render :edit
end
def remove_wip
@@ -277,6 +361,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
status = pipeline.status
coverage = pipeline.try(:coverage)
+ status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
+
status ||= "preparing"
else
ci_service = @merge_request.source_project.ci_service
@@ -308,7 +394,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_request
- @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
+ @issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
alias_method :subscribable_resource, :merge_request
alias_method :issuable, :merge_request
@@ -322,6 +408,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
end
+ def authorize_can_resolve_conflicts!
+ return render_404 unless @merge_request.conflicts_can_be_resolved_by?(current_user)
+ end
+
def module_enabled
return render_404 unless @project.merge_requests_enabled
end
@@ -346,37 +436,35 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits_count = @merge_request.commits.count
@pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses if @pipeline
+ @statuses = @pipeline.statuses.relevant if @pipeline
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
-
- if request.format == :html || action_name == 'show'
- define_show_html_vars
- end
end
- # Discussion tab data is only required on html requests
- def define_show_html_vars
+ # Discussion tab data is rendered on html responses of actions
+ # :show, :diff, :commits, :builds. but not when request the data through AJAX
+ def define_discussion_vars
# Build a note object for comment form
- @note = @project.notes.new(noteable: @noteable)
+ @note = @project.notes.new(noteable: @merge_request)
- @discussions = @noteable.mr_and_commit_notes.
- inc_author_project_award_emoji.
- fresh.
- discussions
+ @discussions = @merge_request.discussions
+
+ preload_noteable_for_regular_notes(@discussions.flat_map(&:notes))
# This is not executed lazily
@notes = Banzai::NoteRenderer.render(
- @discussions.flatten,
+ @discussions.flat_map(&:notes),
@project,
current_user,
@path,
@project_wiki,
@ref
)
+
+ preload_max_access_for_authors(@notes, @project)
end
def define_widget_vars
@@ -384,6 +472,30 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@pipelines = [@pipeline].compact
end
+ def define_commit_vars
+ @commit = @merge_request.diff_head_commit
+ @base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
+ end
+
+ def define_diff_comment_vars
+ @comments_target = {
+ noteable_type: 'MergeRequest',
+ noteable_id: @merge_request.id
+ }
+
+ @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs?
+ @grouped_diff_discussions = @merge_request.notes.inc_relations_for_view.grouped_diff_discussions
+
+ Banzai::NoteRenderer.render(
+ @grouped_diff_discussions.values.flat_map(&:notes),
+ @project,
+ current_user,
+ @path,
+ @project_wiki,
+ @ref
+ )
+ end
+
def invalid_mr
# Render special view for MR with removed source or target branch
render 'invalid'
@@ -394,7 +506,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, :force_remove_source_branch,
- label_ids: []
+ :lock_version, label_ids: []
)
end
@@ -412,4 +524,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_when_build_succeeds].present? &&
@merge_request.pipeline && @merge_request.pipeline.active?
end
+
+ def build_merge_request
+ params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
+ @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ end
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 3eacdbbd067..0948ad21649 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -5,6 +5,7 @@ class Projects::NotesController < Projects::ApplicationController
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
+ before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
before_action :find_current_user_notes, only: [:index]
def index
@@ -66,6 +67,33 @@ class Projects::NotesController < Projects::ApplicationController
end
end
+ def resolve
+ return render_404 unless note.resolvable?
+
+ note.resolve!(current_user)
+
+ MergeRequests::ResolvedDiscussionNotificationService.new(project, current_user).execute(note.noteable)
+
+ discussion = note.discussion
+
+ render json: {
+ resolved_by: note.resolved_by.try(:name),
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
+
+ def unresolve
+ return render_404 unless note.resolvable?
+
+ note.unresolve!
+
+ discussion = note.discussion
+
+ render json: {
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
+
private
def note
@@ -73,7 +101,7 @@ class Projects::NotesController < Projects::ApplicationController
end
alias_method :awardable, :note
- def note_to_html(note)
+ def note_html(note)
render_to_string(
"projects/notes/_note",
layout: false,
@@ -82,20 +110,20 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def note_to_discussion_html(note)
- return unless note.diff_note?
+ def diff_discussion_html(discussion)
+ return unless discussion.diff_discussion?
if params[:view] == 'parallel'
- template = "projects/notes/_diff_notes_with_reply_parallel"
+ template = "discussions/_parallel_diff_discussion"
locals =
if params[:line_type] == 'old'
- { notes_left: [note], notes_right: [] }
+ { discussion_left: discussion, discussion_right: nil }
else
- { notes_left: [], notes_right: [note] }
+ { discussion_left: nil, discussion_right: discussion }
end
else
- template = "projects/notes/_diff_notes_with_reply"
- locals = { notes: [note] }
+ template = "discussions/_diff_discussion"
+ locals = { discussion: discussion }
end
render_to_string(
@@ -106,14 +134,14 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def note_to_discussion_with_diff_html(note)
- return unless note.diff_note?
+ def discussion_html(discussion)
+ return unless discussion.diff_discussion?
render_to_string(
- "projects/notes/_discussion",
+ "discussions/_discussion",
layout: false,
formats: [:html],
- locals: { discussion_notes: [note] }
+ locals: { discussion: discussion }
)
end
@@ -125,33 +153,40 @@ class Projects::NotesController < Projects::ApplicationController
id: note.id,
name: note.name
}
- elsif note.valid?
+ elsif note.persisted?
Banzai::NoteRenderer.render([note], @project, current_user)
attrs = {
valid: true,
id: note.id,
discussion_id: note.discussion_id,
- html: note_to_html(note),
+ html: note_html(note),
award: false,
- note: note.note,
- discussion_html: note_to_discussion_html(note),
- discussion_with_diff_html: note_to_discussion_with_diff_html(note)
+ note: note.note
}
- # The discussion_id is used to add the comment to the correct discussion
- # element on the merge request page. Among other things, the discussion_id
- # contains the sha of head commit of the merge request.
- # When new commits are pushed into the merge request after the initial
- # load of the merge request page, the discussion elements will still have
- # the old discussion_ids, with the old head commit sha. The new comment,
- # however, will have the new discussion_id with the new commit sha.
- # To ensure that these new comments will still end up in the correct
- # discussion element, we also send the original discussion_id, with the
- # old commit sha, along, and fall back on this value when no discussion
- # element with the new discussion_id could be found.
- if note.new_diff_note? && note.position != note.original_position
- attrs[:original_discussion_id] = note.original_discussion_id
+ if note.diff_note?
+ discussion = note.to_discussion
+
+ attrs.merge!(
+ diff_discussion_html: diff_discussion_html(discussion),
+ discussion_html: discussion_html(discussion)
+ )
+
+ # The discussion_id is used to add the comment to the correct discussion
+ # element on the merge request page. Among other things, the discussion_id
+ # contains the sha of head commit of the merge request.
+ # When new commits are pushed into the merge request after the initial
+ # load of the merge request page, the discussion elements will still have
+ # the old discussion_ids, with the old head commit sha. The new comment,
+ # however, will have the new discussion_id with the new commit sha.
+ # To ensure that these new comments will still end up in the correct
+ # discussion element, we also send the original discussion_id, with the
+ # old commit sha, along, and fall back on this value when no discussion
+ # element with the new discussion_id could be found.
+ if note.new_diff_note? && note.position != note.original_position
+ attrs[:original_discussion_id] = note.original_discussion_id
+ end
end
attrs
@@ -168,6 +203,10 @@ class Projects::NotesController < Projects::ApplicationController
return access_denied! unless can?(current_user, :admin_note, note)
end
+ def authorize_resolve_note!
+ return access_denied! unless can?(current_user, :resolve_note, note)
+ end
+
def note_params
params.require(:note).permit(
:note, :noteable, :noteable_id, :noteable_type, :project_id,
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 487963fdcd7..b0c72cfe4b4 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -19,7 +19,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
- @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute
+ @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
new file mode 100644
index 00000000000..9136633b87a
--- /dev/null
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -0,0 +1,36 @@
+class Projects::PipelinesSettingsController < Projects::ApplicationController
+ before_action :authorize_admin_pipeline!
+
+ def show
+ @ref = params[:ref] || @project.default_branch || 'master'
+
+ @badges = [Gitlab::Badge::Build::Status,
+ Gitlab::Badge::Coverage::Report]
+
+ @badges.map! do |badge|
+ badge.new(@project, @ref).metadata
+ end
+ end
+
+ def update
+ if @project.update_attributes(update_params)
+ flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
+ redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
+ else
+ render 'index'
+ end
+ end
+
+ private
+
+ def create_params
+ params.require(:pipeline).permit(:ref)
+ end
+
+ def update_params
+ params.require(:project).permit(
+ :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
+ :public_builds
+ )
+ end
+end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a118964..42a7e5a2c30 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
@@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
- params.require(:project_member).permit(:user_id, :access_level)
+ params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 80dad758afa..9a438d5512c 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -3,19 +3,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
+ before_action :load_protected_branches, only: [:index]
layout "project_settings"
def index
- @protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
- gon.push({ open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } })
+ load_gon_index
end
def create
- @project.protected_branches.create(protected_branch_params)
- redirect_to namespace_project_protected_branches_path(@project.namespace,
- @project)
+ @protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
+ if @protected_branch.persisted?
+ redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
+ else
+ load_protected_branches
+ load_gon_index
+ render :index
+ end
end
def show
@@ -23,7 +28,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def update
- if @protected_branch && @protected_branch.update_attributes(protected_branch_params)
+ @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
+
+ if @protected_branch.valid?
respond_to do |format|
format.json { render json: @protected_branch, status: :ok }
end
@@ -50,6 +57,24 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
end
def protected_branch_params
- params.require(:protected_branch).permit(:name, :developers_can_push)
+ params.require(:protected_branch).permit(:name,
+ merge_access_levels_attributes: [:access_level, :id],
+ push_access_levels_attributes: [:access_level, :id])
+ end
+
+ def load_protected_branches
+ @protected_branches = @project.protected_branches.order(:name).page(params[:page])
+ end
+
+ def access_levels_options
+ {
+ push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
+ merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
+ }
+ end
+
+ def load_gon_index
+ params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
+ gon.push(params.merge(access_levels_options))
end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index d79f16e6a5a..3602b3d5e58 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -25,7 +25,7 @@ class Projects::RefsController < Projects::ApplicationController
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
when "badges"
- namespace_project_badges_path(@project.namespace, @project, ref: @id)
+ namespace_project_pipelines_settings_path(@project.namespace, @project, ref: @id)
else
namespace_project_commits_path(@project.namespace, @project, @id)
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 739681f4085..6a227d85f6f 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -1,20 +1,5 @@
class Projects::ServicesController < Projects::ApplicationController
- ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :drone_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
- :colorize_messages, :channels,
- :push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :build_events, :wiki_page_events,
- :notify_only_broken_builds, :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]
-
- # Parameters to ignore if no value is specified
- FILTER_BLANK_PARAMS = [:password]
+ include ServiceParams
# Authorize
before_action :authorize_admin_project!
@@ -33,7 +18,7 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- if @service.update_attributes(service_params)
+ if @service.update_attributes(service_params[:service])
redirect_to(
edit_namespace_project_service_path(@project.namespace, @project,
@service.to_param, notice:
@@ -45,8 +30,9 @@ class Projects::ServicesController < Projects::ApplicationController
end
def test
- data = Gitlab::PushDataBuilder.build_sample(project, current_user)
+ data = @service.test_data(project, current_user)
outcome = @service.test(data)
+
if outcome[:success]
message = { notice: 'We sent a request to the provided URL' }
else
@@ -63,12 +49,4 @@ class Projects::ServicesController < Projects::ApplicationController
def service
@service ||= @project.services.find { |service| service.to_param == params[:id] }
end
-
- def service_params
- service_params = params.require(:service).permit(ALLOWED_PARAMS)
- FILTER_BLANK_PARAMS.each do |param|
- service_params.delete(param) if service_params[param].blank?
- end
- service_params
- end
end
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 6dc495247c8..8592579abbd 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -10,11 +10,12 @@ class Projects::TagsController < Projects::ApplicationController
@tags = @repository.tags_sorted_by(@sort)
@tags = Kaminari.paginate_array(@tags).page(params[:page])
- @releases = project.releases.where(tag: @tags)
+ @releases = project.releases.where(tag: @tags.map(&:name))
end
def show
@tag = @repository.find_tag(params[:id])
+
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
@commit = @repository.commit(@tag.target)
end
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
new file mode 100644
index 00000000000..694b468c8d3
--- /dev/null
+++ b/app/controllers/projects/templates_controller.rb
@@ -0,0 +1,19 @@
+class Projects::TemplatesController < Projects::ApplicationController
+ before_action :authenticate_user!, :get_template_class
+
+ def show
+ template = @template_type.find(params[:key], project)
+
+ respond_to do |format|
+ format.json { render json: template.to_json }
+ end
+ end
+
+ private
+
+ def get_template_class
+ template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
+ @template_type = template_types[params[:template_type]]
+ render json: [], status: 404 unless @template_type
+ end
+end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 23868d986e9..5685d0f4e7c 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -5,7 +5,7 @@ class Projects::TodosController < Projects::ApplicationController
todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
- count: current_user.todos_pending_count,
+ count: TodosFinder.new(current_user, state: :pending).execute.count,
delete_path: dashboard_todo_path(todo)
}
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index caed064dfbc..e617be8f9fb 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,6 +1,6 @@
class Projects::UploadsController < Projects::ApplicationController
skip_before_action :reject_blocked!, :project,
- :repository, if: -> { action_name == 'show' && image? }
+ :repository, if: -> { action_name == 'show' && image_or_video? }
before_action :authorize_upload_file!, only: [:create]
@@ -24,7 +24,7 @@ class Projects::UploadsController < Projects::ApplicationController
def show
return render_404 if uploader.nil? || !uploader.file.exists?
- disposition = uploader.image? ? 'inline' : 'attachment'
+ disposition = uploader.image_or_video? ? 'inline' : 'attachment'
send_file uploader.file.path, disposition: disposition
end
@@ -49,7 +49,7 @@ class Projects::UploadsController < Projects::ApplicationController
@uploader
end
- def image?
- uploader && uploader.file.exists? && uploader.image?
+ def image_or_video?
+ uploader && uploader.file.exists? && uploader.image_or_video?
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 607fe9c7fed..177ccf5eec9 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -91,7 +91,7 @@ class Projects::WikisController < Projects::ApplicationController
)
end
- def markdown_preview
+ def preview_markdown
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 1803aa8eab4..fc52cd2f367 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -5,7 +5,7 @@ class ProjectsController < Projects::ApplicationController
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, only: [:show], if: :repo_exists?
- before_action :tree, only: [:show], if: :project_view_files?
+ before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -97,7 +97,7 @@ class ProjectsController < Projects::ApplicationController
end
if @project.pending_delete?
- flash[:alert] = "Project queued for delete."
+ flash[:alert] = "Project #{@project.name} queued for deletion."
end
respond_to do |format|
@@ -125,7 +125,7 @@ class ProjectsController < Projects::ApplicationController
def destroy
return access_denied! unless can?(current_user, :remove_project, @project)
- ::Projects::DestroyService.new(@project, current_user, {}).pending_delete!
+ ::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:alert] = "Project '#{@project.name}' will be deleted."
redirect_to dashboard_projects_path
@@ -134,10 +134,22 @@ class ProjectsController < Projects::ApplicationController
end
def autocomplete_sources
- note_type = params['type']
- note_id = params['type_id']
+ noteable =
+ case params[:type]
+ when 'Issue'
+ IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
+ execute.find_by(iid: params[:type_id])
+ when 'MergeRequest'
+ MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
+ 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(note_type, note_id)
+ participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
@suggestions = {
emojis: Gitlab::AwardEmoji.urls,
@@ -145,7 +157,8 @@ class ProjectsController < Projects::ApplicationController
milestones: autocomplete.milestones,
mergerequests: autocomplete.merge_requests,
labels: autocomplete.labels,
- members: participants
+ members: participants,
+ commands: autocomplete.commands(noteable, params[:type])
}
respond_to do |format|
@@ -238,7 +251,7 @@ class ProjectsController < Projects::ApplicationController
}
end
- def markdown_preview
+ def preview_markdown
text = params[:text]
ext = Gitlab::ReferenceExtractor.new(@project, current_user)
@@ -296,7 +309,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :only_allow_merge_if_build_succeeds
+ :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
)
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 75b78a49eab..3327f4f2b87 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -33,7 +33,7 @@ class RegistrationsController < Devise::RegistrationsController
protected
- def build_resource(hash=nil)
+ def build_resource(hash = nil)
super
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 69c92d2bed2..61517d21f9f 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,5 +1,5 @@
class SearchController < ApplicationController
- skip_before_action :authenticate_user!, :reject_blocked
+ skip_before_action :authenticate_user!, :reject_blocked!
include SearchHelper
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 17aed816cbd..5d7ecfeacf4 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -101,7 +101,7 @@ class SessionsController < Devise::SessionsController
# Prevent alert from popping up on the first page shown after authentication.
flash[:alert] = nil
- redirect_to user_omniauth_authorize_path(provider.to_sym)
+ redirect_to omniauth_authorize_path(:user, provider)
end
def valid_otp_attempt?(user)
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
new file mode 100644
index 00000000000..533076585c0
--- /dev/null
+++ b/app/finders/branches_finder.rb
@@ -0,0 +1,31 @@
+class BranchesFinder
+ def initialize(repository, params)
+ @repository = repository
+ @params = params
+ end
+
+ def execute
+ branches = @repository.branches_sorted_by(sort)
+ filter_by_name(branches)
+ end
+
+ private
+
+ attr_reader :repository, :params
+
+ def search
+ @params[:search].presence
+ end
+
+ def sort
+ @params[:sort].presence || 'name'
+ end
+
+ def filter_by_name(branches)
+ if search
+ branches.select { |branch| branch.name.include?(search) }
+ else
+ branches
+ end
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index a0932712bd0..33daac0399e 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -109,7 +109,7 @@ class IssuableFinder
scope.where(title: params[:milestone_title])
else
- nil
+ Milestone.none
end
end
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
new file mode 100644
index 00000000000..79eb45568be
--- /dev/null
+++ b/app/finders/move_to_project_finder.rb
@@ -0,0 +1,20 @@
+class MoveToProjectFinder
+ PAGE_SIZE = 50
+
+ def initialize(user)
+ @user = user
+ end
+
+ def execute(from_project, search: nil, offset_id: nil)
+ projects = @user.projects_where_can_admin_issues
+ projects = projects.search(search) if search.present?
+ projects = projects.excluding_project(from_project)
+
+ # infinite scroll using offset
+ projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
+ projects = projects.limit(PAGE_SIZE)
+
+ # to ask for Project#name_with_namespace
+ projects.includes(namespace: :owner)
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 2f0a9659d15..c7911736812 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,6 +1,7 @@
class ProjectsFinder < UnionFinder
- def execute(current_user = nil, options = {})
+ def execute(current_user = nil, project_ids_relation = nil)
segments = all_projects(current_user)
+ segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
find_union(segments, Project)
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 7806d9e4cc5..06b3e8a9502 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -8,7 +8,7 @@
# action_id: integer
# author_id: integer
# project_id; integer
-# state: 'pending' or 'done'
+# state: 'pending' (default) or 'done'
# type: 'Issue' or 'MergeRequest'
#
@@ -17,7 +17,7 @@ class TodosFinder
attr_accessor :current_user, :params
- def initialize(current_user, params)
+ def initialize(current_user, params = {})
@current_user = current_user
@params = params
end
@@ -27,17 +27,19 @@ class TodosFinder
items = by_action_id(items)
items = by_action(items)
items = by_author(items)
- items = by_project(items)
items = by_state(items)
items = by_type(items)
+ # Filtering by project HAS TO be the last because we use
+ # the project IDs yielded by the todos query thus far
+ items = by_project(items)
- items.reorder(id: :desc)
+ sort(items)
end
private
def action_id?
- action_id.present? && [Todo::ASSIGNED, Todo::MENTIONED, Todo::BUILD_FAILED, Todo::MARKED].include?(action_id.to_i)
+ action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end
def action_id
@@ -91,14 +93,9 @@ class TodosFinder
@project
end
- def projects
- return @projects if defined?(@projects)
-
- if project?
- @projects = project
- else
- @projects = ProjectsFinder.new.execute(current_user)
- end
+ def projects(items)
+ item_project_ids = items.reorder(nil).select(:project_id)
+ ProjectsFinder.new.execute(current_user, item_project_ids)
end
def type?
@@ -109,6 +106,10 @@ class TodosFinder
params[:type]
end
+ def sort(items)
+ params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ end
+
def by_action(items)
if action?
items = items.where(action: to_action_id)
@@ -136,8 +137,9 @@ class TodosFinder
def by_project(items)
if project?
items = items.where(project: project)
- elsif projects
- items = items.merge(projects).joins(:project)
+ else
+ item_projects = projects(items)
+ items = items.merge(item_projects).joins(:project)
end
items
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index 950f323e383..de13e7a1fc2 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -31,7 +31,9 @@ module AppearancesHelper
end
end
- def navbar_icon(icon_name, size: 16)
+ def custom_icon(icon_name, size: 16)
+ # We can't simply do the below, because there are some .erb SVGs.
+ # File.read(Rails.root.join("app/views/shared/icons/_#{icon_name}.svg")).html_safe
render "shared/icons/#{icon_name}.svg", size: size
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 03495cf5ec4..f3733b01721 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -163,9 +163,13 @@ module ApplicationHelper
# `html_class` argument is provided.
#
# Returns an HTML-safe String
- def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
+ def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, 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: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}",
+ 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' }
@@ -245,7 +249,6 @@ module ApplicationHelper
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
- sort: params[:sort],
issue_search: params[:issue_search],
label_name: params[:label_name]
}
@@ -317,4 +320,8 @@ module ApplicationHelper
capture(&block)
end
end
+
+ def page_class
+ "issue-boards-page" if current_controller?(:boards)
+ end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 78c0b79d2bd..6de25bea654 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -31,6 +31,10 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
+ def koding_enabled?
+ current_application_settings.koding_enabled?
+ end
+
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
new file mode 100644
index 00000000000..aa8acbe7567
--- /dev/null
+++ b/app/helpers/avatars_helper.rb
@@ -0,0 +1,26 @@
+module AvatarsHelper
+ def author_avatar(commit_or_event, options = {})
+ user_avatar(options.merge({
+ user: commit_or_event.author,
+ user_name: commit_or_event.author_name,
+ user_email: commit_or_event.author_email,
+ }))
+ end
+
+ def user_avatar(options = {})
+ avatar_size = options[:size] || 16
+ user_name = options[:user].try(:name) || options[:user_name]
+ avatar = image_tag(
+ avatar_icon(options[:user] || options[:user_email], avatar_size),
+ class: "avatar has-tooltip hidden-xs s#{avatar_size}",
+ alt: "#{user_name}'s avatar",
+ title: user_name
+ )
+
+ if options[:user]
+ link_to(avatar, user_path(options[:user]))
+ elsif options[:user_email]
+ mail_to(options[:user_email], avatar)
+ end
+ end
+end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 428a42266d0..e13b7cdd707 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -1,10 +1,7 @@
module BlobHelper
- def highlighter(blob_name, blob_content, repository: nil, nowrap: false)
- Gitlab::Highlight.new(blob_name, blob_content, nowrap: nowrap, repository: repository)
- end
-
- def highlight(blob_name, blob_content, repository: nil, nowrap: false, plain: false)
- Gitlab::Highlight.highlight(blob_name, blob_content, nowrap: nowrap, plain: plain, repository: repository)
+ def highlight(blob_name, blob_content, repository: nil, plain: false)
+ highlighted = Gitlab::Highlight.highlight(blob_name, blob_content, plain: plain, repository: repository)
+ raw %(<pre class="code highlight"><code>#{highlighted}</code></pre>)
end
def no_highlight_files
@@ -14,17 +11,14 @@ module BlobHelper
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
return unless current_user
- blob = project.repository.blob_at(ref, path) rescue nil
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
- return unless blob && blob_text_viewable?(blob)
-
- from_mr = options[:from_merge_request_id]
- link_opts = {}
- link_opts[:from_merge_request_id] = from_mr if from_mr
+ return unless blob
edit_path = namespace_project_edit_blob_path(project.namespace, project,
tree_join(ref, path),
- link_opts)
+ options[:link_opts])
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
@@ -185,17 +179,50 @@ module BlobHelper
}
end
+ def selected_template(issuable)
+ templates = issuable_templates(issuable)
+ params[:issuable_template] if templates.include?(params[:issuable_template])
+ end
+
+ def can_add_template?(issuable)
+ names = issuable_templates(issuable)
+ names.empty? && can?(current_user, :push_code, @project) && !@project.private?
+ end
+
+ def merge_request_template_names
+ @merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
+ end
+
+ def issue_template_names
+ @issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
+ end
+
+ def issuable_templates(issuable)
+ @issuable_templates ||=
+ if issuable.is_a?(Issue)
+ issue_template_names
+ elsif issuable.is_a?(MergeRequest)
+ merge_request_template_names
+ end
+ end
+
+ def ref_project
+ @ref_project ||= @target_project || @project
+ end
+
def gitignore_names
- @gitignore_names ||=
- Gitlab::Template::Gitignore.categories.keys.map do |k|
- [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
- end.to_h
+ @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names
end
def gitlab_ci_ymls
- @gitlab_ci_ymls ||=
- Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
- [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
- end.to_h
+ @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
+ end
+
+ def blob_editor_paths
+ {
+ 'relative-url-root' => Rails.application.config.relative_url_root,
+ 'assets-prefix' => Gitlab::Application.config.assets.prefix,
+ 'blob-language' => @blob && @blob.language.try(:ace_mode)
+ }
end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 601df5c18df..3fc85dc6b2b 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -9,10 +9,21 @@ module BranchesHelper
end
end
+ def filter_branches_path(options = {})
+ exist_opts = {
+ search: params[:search],
+ sort: params[:sort]
+ }
+
+ options = exist_opts.merge(options)
+
+ namespace_project_branches_path(@project.namespace, @project, @id, options)
+ end
+
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_exists?(branch_name)
- ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(branch_name)
+ ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch_name)
end
def project_branches
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 0f097f86816..b478580978b 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -15,29 +15,13 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
+ data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn btn-clipboard",
data: data,
- type: :button
- end
-
- # Output a "Copy to Clipboard" button with a custom CSS class
- #
- # data - Data attributes passed to `content_tag`
- # css_class - Class passed to the `content_tag`
- #
- # Examples:
- #
- # # Define the target element
- # clipboard_button_with_class({clipboard_target: "div#foo"}, css_class: "btn-clipboard")
- # # => "<button class='btn btn-clipboard' data-clipboard-target='div#foo'>...</button>"
- def clipboard_button_with_class(data = {}, css_class: 'btn-clipboard')
- content_tag :button,
- icon('clipboard'),
- class: "btn #{css_class}",
- data: data,
- type: :button
+ type: :button,
+ title: "Copy to Clipboard"
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 8e4ae1e6aec..0327b476d18 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -15,8 +15,11 @@ module CiStatusHelper
end
def ci_label_for_status(status)
- if status == 'success'
+ case status
+ when 'success'
'passed'
+ when 'success_with_warnings'
+ 'passed with warnings'
else
status
end
@@ -26,28 +29,36 @@ module CiStatusHelper
icon_name =
case status
when 'success'
- 'check'
+ 'icon_status_success'
+ when 'success_with_warnings'
+ 'icon_status_warning'
when 'failed'
- 'close'
- when 'running', 'pending'
- 'clock-o'
+ 'icon_status_failed'
+ when 'pending'
+ 'icon_status_pending'
+ when 'running'
+ 'icon_status_running'
+ when 'play'
+ 'icon_play'
+ when 'created'
+ 'icon_status_pending'
else
- 'circle'
+ 'icon_status_cancel'
end
- icon(icon_name + ' fw')
+ custom_icon(icon_name)
end
- def render_commit_status(commit, tooltip_placement: 'auto left', cssclass: '')
+ def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
path = builds_namespace_project_commit_path(project.namespace, project, commit)
- render_status_with_link('commit', commit.status, path, tooltip_placement, cssclass: cssclass)
+ render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end
def render_pipeline_status(pipeline, tooltip_placement: 'auto left')
project = pipeline.project
path = namespace_project_pipeline_path(project.namespace, project, pipeline)
- render_status_with_link('pipeline', pipeline.status, path, tooltip_placement)
+ render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement)
end
def no_runners_for_project?(project)
@@ -55,13 +66,17 @@ module CiStatusHelper
Ci::Runner.shared.blank?
end
- private
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
+ klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
+ title = "#{type.titleize}: #{ci_label_for_status(status)}"
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
- def render_status_with_link(type, status, path, tooltip_placement, cssclass: '')
- link_to ci_icon_for_status(status),
- path,
- class: "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}",
- title: "#{type.titleize}: #{ci_label_for_status(status)}",
- data: { toggle: 'tooltip', placement: tooltip_placement }
+ if path
+ link_to ci_icon_for_status(status), path,
+ class: klass, title: title, data: data
+ else
+ content_tag :span, ci_icon_for_status(status),
+ class: klass, title: title, data: data
+ end
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 474041eccbb..33dcee49aee 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
module CommitsHelper
# Returns a link to the commit author. If the author has a matching user and
# is a member of the current @project it will link to the team member page.
@@ -16,16 +15,6 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
- def commit_author_avatar(commit, options = {})
- options = options.merge(source: :author)
- user = commit.send(options[:source])
-
- source_email = clean(commit.send "#{options[:source]}_email".to_sym)
- person_email = user.try(:email) || source_email
-
- image_tag(avatar_icon(person_email, options[:size]), class: "avatar #{"s#{options[:size]}" if options[:size]} hidden-xs", width: options[:size], alt: "")
- end
-
def image_diff_class(diff)
if diff.deleted_file
"deleted"
@@ -109,28 +98,31 @@ module CommitsHelper
end
def link_to_browse_code(project, commit)
- if current_controller?(:projects, :commits)
- if @repo.blob_at(commit.id, @path)
- return link_to(
- "Browse File",
- namespace_project_blob_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
- elsif @path.present?
- return link_to(
- "Browse Directory",
- namespace_project_tree_path(project.namespace, project,
- tree_join(commit.id, @path)),
- class: "btn btn-default"
- )
- end
+ if @path.blank?
+ return link_to(
+ "Browse Files",
+ namespace_project_tree_path(project.namespace, project, commit),
+ class: "btn btn-default"
+ )
+ end
+
+ return unless current_controller?(:projects, :commits)
+
+ if @repo.blob_at(commit.id, @path)
+ return link_to(
+ "Browse File",
+ namespace_project_blob_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "btn btn-default"
+ )
+ elsif @path.present?
+ return link_to(
+ "Browse Directory",
+ namespace_project_tree_path(project.namespace, project,
+ tree_join(commit.id, @path)),
+ class: "btn btn-default"
+ )
end
- link_to(
- "Browse Files",
- namespace_project_tree_path(project.namespace, project, commit),
- class: "btn btn-default"
- )
end
def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true)
@@ -217,10 +209,10 @@ module CommitsHelper
end
end
- def view_file_btn(commit_sha, diff, project)
+ def view_file_btn(commit_sha, diff_new_path, project)
link_to(
namespace_project_blob_path(project.namespace, project,
- tree_join(commit_sha, diff.new_path)),
+ tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file btn-file-option'
) do
raw('View file @') + content_tag(:span, commit_sha[0..6],
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index eb57516247d..0725c3f4c56 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -8,38 +8,47 @@ module DiffHelper
[marked_old_line, marked_new_line]
end
- def diff_view
- diff_views = %w(inline parallel)
-
- if diff_views.include?(cookies[:diff_view])
- cookies[:diff_view]
- else
- diff_views.first
- end
+ def expand_all_diffs?
+ params[:expand_all_diffs].present?
end
- def diff_hard_limit_enabled?
- params[:force_show_diff].present?
+ def diff_view
+ @diff_view ||= begin
+ diff_views = %w(inline parallel)
+ diff_view = cookies[:diff_view]
+ diff_view = diff_views.first unless diff_views.include?(diff_view)
+ diff_view.to_sym
+ end
end
def diff_options
- options = { ignore_whitespace_change: hide_whitespace? }
- if diff_hard_limit_enabled?
- options.merge!(Commit.max_diff_options)
+ options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? }
+
+ if action_name == 'diff_for_path'
+ options[:no_collapse] = true
+ options[:paths] = params.values_at(:old_path, :new_path)
end
+
options
end
- def safe_diff_files(diffs, diff_refs: nil, repository: nil)
- diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
- end
+ def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false)
+ content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}"
+ cls = ['diff-line-num', 'unfold', 'js-unfold']
+ cls << 'js-unfold-bottom' if bottom
- def unfold_bottom_class(bottom)
- bottom ? 'js-unfold-bottom' : ''
- end
+ html = ''
+ if old_pos
+ html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos })
+ html << content unless view == :inline
+ end
+
+ if new_pos
+ html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos })
+ html << content
+ end
- def unfold_class(unfold)
- unfold ? 'unfold js-unfold' : ''
+ html.html_safe
end
def diff_line_content(line, line_type = nil)
@@ -51,26 +60,28 @@ module DiffHelper
end
end
- def organize_comments(left, right)
- notes_left = notes_right = nil
+ def parallel_diff_discussions(left, right, diff_file)
+ discussion_left = discussion_right = nil
- unless left[:type].nil? && right[:type] == 'new'
- notes_left = @grouped_diff_notes[left[:line_code]]
+ if left && (left.unchanged? || left.removed?)
+ line_code = diff_file.line_code(left)
+ discussion_left = @grouped_diff_discussions[line_code]
end
- unless left[:type].nil? && right[:type].nil?
- notes_right = @grouped_diff_notes[right[:line_code]]
+ if right && right.added?
+ line_code = diff_file.line_code(right)
+ discussion_right = @grouped_diff_discussions[line_code]
end
- [notes_left, notes_right]
+ [discussion_left, discussion_right]
end
def inline_diff_btn
- diff_btn('Inline', 'inline', diff_view == 'inline')
+ diff_btn('Inline', 'inline', diff_view == :inline)
end
def parallel_diff_btn
- diff_btn('Side-by-side', 'parallel', diff_view == 'parallel')
+ diff_btn('Side-by-side', 'parallel', diff_view == :parallel)
end
def submodule_link(blob, ref, repository = @repository)
@@ -90,7 +101,7 @@ module DiffHelper
def commit_for_diff(diff_file)
return diff_file.content_commit if diff_file.content_commit
-
+
if diff_file.deleted_file
@base_commit || @commit.parent || @commit
else
@@ -98,11 +109,11 @@ module DiffHelper
end
end
- def diff_file_html_data(project, diff_file)
- commit = commit_for_diff(diff_file)
+ def diff_file_html_data(project, diff_file_path, diff_commit_id)
{
blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
- tree_join(commit.id, diff_file.file_path))
+ tree_join(diff_commit_id, diff_file_path)),
+ view: diff_view
}
end
@@ -139,8 +150,6 @@ module DiffHelper
toggle_whitespace_link(url, options)
end
- private
-
def hide_whitespace?
params[:w] == '1'
end
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
index 337b0aacbb5..2b1f3825adc 100644
--- a/app/helpers/explore_helper.rb
+++ b/app/helpers/explore_helper.rb
@@ -1,5 +1,5 @@
module ExploreHelper
- def filter_projects_path(options={})
+ def filter_projects_path(options = {})
exist_opts = {
sort: params[:sort],
scope: params[:scope],
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
index 1f3401f2906..defd87d6bbe 100644
--- a/app/helpers/external_wiki_helper.rb
+++ b/app/helpers/external_wiki_helper.rb
@@ -1,8 +1,7 @@
module ExternalWikiHelper
def get_project_wiki_path(project)
- external_wiki_service = project.services.
- find { |service| service.to_param == 'external_wiki' }
- if external_wiki_service.present? && external_wiki_service.active?
+ external_wiki_service = project.external_wiki
+ if external_wiki_service
external_wiki_service.properties['external_wiki_url']
else
namespace_project_wiki_path(project.namespace, project, :home)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 294b7e92b8d..b9baeb1d6c4 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -61,7 +61,7 @@ module IssuablesHelper
output = content_tag :strong, "#{text} #{issuable.to_reference}", class: "identifier"
output << " opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe
output << content_tag(:strong) do
- author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs")
+ author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
end
@@ -72,6 +72,15 @@ module IssuablesHelper
end
end
+ def issuable_labels_tooltip(labels, limit: 5)
+ first, last = labels.partition.with_index{ |_, i| i < limit }
+
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
+
+ label_names.join(', ')
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 2b0defd1dda..8b212b0327a 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -13,38 +13,6 @@ module IssuesHelper
OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
end
- def url_for_project_issues(project = @project, options = {})
- return '' if project.nil?
-
- url =
- if options[:only_path]
- project.issues_tracker.project_path
- else
- project.issues_tracker.project_url
- end
-
- # Ensure we return a valid URL to prevent possible XSS.
- URI.parse(url).to_s
- rescue URI::InvalidURIError
- ''
- end
-
- def url_for_new_issue(project = @project, options = {})
- return '' if project.nil?
-
- url =
- if options[:only_path]
- project.issues_tracker.new_issue_path
- else
- project.issues_tracker.new_issue_url
- end
-
- # Ensure we return a valid URL to prevent possible XSS.
- URI.parse(url).to_s
- rescue URI::InvalidURIError
- ''
- end
-
def url_for_issue(issue_iid, project = @project, options = {})
return '' if project.nil?
@@ -146,9 +114,17 @@ module IssuesHelper
end
def award_user_list(awards, current_user)
- awards.map do |award|
- award.user == current_user ? 'me' : award.user.name
- end.join(', ')
+ names = awards.map do |award|
+ award.user == current_user ? 'You' : award.user.name
+ end
+
+ # Take first 9 OR current user + first 9
+ current_user_name = names.delete('You')
+ names = names.first(9).insert(0, current_user_name).compact
+
+ names << "#{awards.size - names.size} more." if awards.size > names.size
+
+ names.to_sentence
end
def award_active_class(awards, current_user)
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
new file mode 100644
index 00000000000..eb651e3687e
--- /dev/null
+++ b/app/helpers/lfs_helper.rb
@@ -0,0 +1,67 @@
+module LfsHelper
+ 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: "#{Gitlab.config.gitlab.url}/help",
+ },
+ 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?
+ project.public? || ci? || (user && user.can?(:download_code, project))
+ end
+
+ def lfs_upload_access?
+ user && user.can?(:push_code, project)
+ end
+
+ def render_lfs_forbidden
+ render(
+ json: {
+ message: 'Access forbidden. Check your access level.',
+ documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ },
+ content_type: "application/vnd.git-lfs+json",
+ status: 403
+ )
+ end
+
+ def render_lfs_not_found
+ render(
+ json: {
+ message: 'Not found.',
+ documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ },
+ 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 ec106418f2d..877c77050be 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,12 +6,6 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym
end
- def default_show_roles(member)
- can?(current_user, action_member_permission(:update, member), member) ||
- can?(current_user, action_member_permission(:destroy, member), member) ||
- can?(current_user, action_member_permission(:admin, member), member.source)
- end
-
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 4da1f4865a4..db6e731c744 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -96,4 +96,8 @@ module MergeRequestsHelper
["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"]
end
end
+
+ def merge_request_button_visibility(merge_request, closed)
+ return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 3ff8be5e284..6c1cc6ef072 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -24,6 +24,7 @@ module NavHelper
current_path?('merge_requests#diffs') ||
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
+ current_path?('merge_requests#conflicts') ||
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 2302e65c537..da230f76bae 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -1,9 +1,4 @@
module NotesHelper
- # Helps to distinguish e.g. commit notes in mr notes list
- def note_for_main_target?(note)
- @noteable.class.name == note.noteable_type && !note.diff_note?
- end
-
def note_target_fields(note)
if note.noteable
hidden_field_tag(:target_type, note.noteable.class.name.underscore) +
@@ -12,7 +7,7 @@ module NotesHelper
end
def note_editable?(note)
- note.editable? && can?(current_user, :admin_note, note)
+ Ability.can_edit_note?(current_user, note)
end
def noteable_json(noteable)
@@ -24,7 +19,15 @@ module NotesHelper
}.to_json
end
- def link_to_new_diff_note(line_code, position, line_type = nil)
+ def diff_view_data
+ return {} unless @comments_target
+
+ @comments_target.slice(:noteable_id, :noteable_type, :commit_id)
+ end
+
+ def diff_view_line_data(line_code, position, line_type)
+ return if @diff_notes_disabled
+
use_legacy_diff_note = @use_legacy_diff_notes
# If the controller doesn't force the use of legacy diff notes, we
# determine this on a line-by-line basis by seeing if there already exist
@@ -36,20 +39,17 @@ module NotesHelper
# If we didn't, diff notes that would show for the same line on the changes
# tab, would show in different discussions on the discussion tab.
use_legacy_diff_note ||= begin
- line_diff_notes = @grouped_diff_notes[line_code]
- line_diff_notes && line_diff_notes.any?(&:legacy_diff_note?)
+ discussion = @grouped_diff_discussions[line_code]
+ discussion && discussion.legacy_diff_discussion?
end
data = {
- noteable_type: @comments_target[:noteable_type],
- noteable_id: @comments_target[:noteable_id],
- commit_id: @comments_target[:commit_id],
- line_type: line_type,
- line_code: line_code
+ line_code: line_code,
+ line_type: line_type,
}
if use_legacy_diff_note
- discussion_id = LegacyDiffNote.build_discussion_id(
+ discussion_id = LegacyDiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
line_code
@@ -60,7 +60,7 @@ module NotesHelper
discussion_id: discussion_id
)
else
- discussion_id = DiffNote.build_discussion_id(
+ discussion_id = DiffNote.discussion_id(
@comments_target[:noteable_type],
@comments_target[:noteable_id] || @comments_target[:commit_id],
position
@@ -73,53 +73,38 @@ module NotesHelper
)
end
- button_tag(class: 'btn add-diff-note js-add-diff-note-button',
- data: data,
- title: 'Add a comment to this line') do
- icon('comment-o')
- end
+ data
end
- def link_to_reply_discussion(note, line_type = nil)
+ def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = {
- noteable_type: note.noteable_type,
- noteable_id: note.noteable_id,
- commit_id: note.commit_id,
- discussion_id: note.discussion_id,
- line_type: line_type
- }
+ data = discussion.reply_attributes.merge(line_type: line_type)
- if note.diff_note?
- data[:note_type] = note.type
+ button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
+ data: data, title: 'Add a reply'
+ end
- data.merge!(note.diff_attributes)
- end
+ def preload_max_access_for_authors(notes, project)
+ user_ids = notes.map(&:author_id)
+ project.team.max_member_access_for_user_ids(user_ids)
+ end
- content_tag(:div, class: "discussion-reply-holder") do
- button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
- data: data, title: 'Add a reply'
- end
+ def preload_noteable_for_regular_notes(notes)
+ ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable)
end
def note_max_access_for_user(note)
- @max_access_by_user_id ||= Hash.new do |hash, key|
- project = key[:project]
- hash[key] = project.team.human_max_access(key[:user_id])
- end
-
- full_key = { project: note.project, user_id: note.author_id }
- @max_access_by_user_id[full_key]
+ note.project.team.human_max_access(note.author_id)
end
- def diff_note_path(note)
- return unless note.diff_note?
+ def discussion_diff_path(discussion)
+ return unless discussion.diff_discussion?
- if note.for_merge_request? && note.active?
- diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
- elsif note.for_commit?
- namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code)
+ if discussion.for_merge_request? && discussion.active?
+ diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
+ elsif discussion.for_commit?
+ namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code)
end
end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 3bbbb26cff2..356f27f2d5d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -19,7 +19,7 @@ module ProjectsHelper
end
def link_to_member(project, author, opts = {}, &block)
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name", tooltip: false }
opts = default_opts.merge(opts)
return "(deleted)" unless author
@@ -33,7 +33,8 @@ module ProjectsHelper
if opts[:by_username]
author_html << content_tag(:span, sanitize("@#{author.username}"), class: opts[:author_class]) if opts[:name]
else
- author_html << content_tag(:span, sanitize(author.name), class: opts[:author_class]) if opts[:name]
+ tooltip_data = { placement: 'top' }
+ author_html << content_tag(:span, sanitize(author.name), class: [opts[:author_class], ('has-tooltip' if opts[:tooltip])], title: (author.to_reference if opts[:tooltip]), data: (tooltip_data if opts[:tooltip])) if opts[:name]
end
author_html << capture(&block) if block
@@ -60,7 +61,7 @@ module ProjectsHelper
project_link = link_to simple_sanitize(project.name), project_path(project), { class: "project-item-select-holder" }
if current_user
- project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
+ project_link << icon("chevron-down", class: "dropdown-toggle-caret js-projects-dropdown-toggle", aria: { label: "Toggle switch project dropdown" }, data: { target: ".js-dropdown-menu-projects", toggle: "dropdown" })
end
full_title = "#{namespace_link} / #{project_link}".html_safe
@@ -115,6 +116,17 @@ module ProjectsHelper
license.nickname || license.name
end
+ def last_push_event
+ return unless current_user
+
+ project_ids = [@project.id]
+ if fork = current_user.fork_of(@project)
+ project_ids << fork.id
+ end
+
+ current_user.recent_push(project_ids)
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -235,6 +247,60 @@ module ProjectsHelper
)
end
+ def add_koding_stack_path(project)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: '.koding.yml',
+ commit_message: "Add Koding stack script",
+ content: <<-CONTENT.strip_heredoc
+ provider:
+ aws:
+ access_key: '${var.aws_access_key}'
+ secret_key: '${var.aws_secret_key}'
+ resource:
+ aws_instance:
+ #{project.path}-vm:
+ instance_type: t2.nano
+ user_data: |-
+
+ # Created by GitLab UI for :>
+
+ echo _KD_NOTIFY_@Installing Base packages...@
+
+ apt-get update -y
+ apt-get install git -y
+
+ echo _KD_NOTIFY_@Cloning #{project.name}...@
+
+ export KODING_USER=${var.koding_user_username}
+ export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
+ export BRANCH=${var.koding_queryString_branch}
+
+ sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
+
+ echo _KD_NOTIFY_@#{project.name} cloned.@
+ CONTENT
+ )
+ end
+
+ def koding_project_url(project = nil, branch = nil, sha = nil)
+ if project
+ import_path = "/Home/Stacks/import"
+
+ repo = project.path_with_namespace
+ branch ||= project.default_branch
+ sha ||= project.commit.short_id
+
+ path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
+
+ return URI.join(current_application_settings.koding_url, path).to_s
+ end
+
+ current_application_settings.koding_url
+ end
+
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
@@ -262,6 +328,10 @@ module ProjectsHelper
filename_path(project, :version)
end
+ def ci_configuration_path(project)
+ filename_path(project, :gitlab_ci_yml)
+ end
+
def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version }
namespace_project_wiki_path(proj.namespace, proj, page, url_params)
@@ -292,16 +362,6 @@ module ProjectsHelper
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
end
- def last_push_event
- return unless current_user
-
- if fork = current_user.fork_of(@project)
- current_user.recent_push(fork.id)
- else
- current_user.recent_push(@project.id)
- end
- end
-
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index f9fc525df6f..4549c2e5bb6 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -43,15 +43,15 @@ module SearchHelper
# Autocomplete results for internal help pages
def help_autocomplete
[
- { category: "Help", label: "API Help", url: help_page_path("api", "README") },
- { category: "Help", label: "Markdown Help", url: help_page_path("markdown", "markdown") },
- { category: "Help", label: "Permissions Help", url: help_page_path("permissions", "permissions") },
- { category: "Help", label: "Public Access Help", url: help_page_path("public_access", "public_access") },
- { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks", "README") },
- { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh", "README") },
- { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks", "system_hooks") },
- { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks", "web_hooks") },
- { category: "Help", label: "Workflow Help", url: help_page_path("workflow", "README") },
+ { category: "Help", label: "API Help", url: help_page_path("api/README") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") },
+ { category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") },
+ { category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
+ { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
+ { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
+ { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
+ { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") },
+ { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") },
]
end
@@ -107,12 +107,13 @@ module SearchHelper
Sanitize.clean(str)
end
- def search_filter_path(options={})
+ def search_filter_path(options = {})
exist_opts = {
search: params[:search],
project_id: params[:project_id],
group_id: params[:group_id],
- scope: params[:scope]
+ scope: params[:scope],
+ repository_ref: params[:repository_ref]
}
options = exist_opts.merge(options)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index bb395e37884..5f27e33c6ad 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -5,21 +5,9 @@ module SelectsHelper
css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
-
- first_user = opts[:first_user] && current_user ? current_user.username : false
-
html = {
class: css_class,
- data: {
- placeholder: opts[:placeholder] || 'Search for a user',
- null_user: opts[:null_user] || false,
- any_user: opts[:any_user] || false,
- email_user: opts[:email_user] || false,
- first_user: first_user,
- current_user: opts[:current_user] || false,
- "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
- author_id: opts[:author_id] || ''
- }
+ data: users_select_data_attributes(opts)
}
unless opts[:scope] == :all
@@ -68,4 +56,20 @@ module SelectsHelper
hidden_field_tag(id, value, class: css_class)
end
+
+ private
+
+ def users_select_data_attributes(opts)
+ {
+ placeholder: opts[:placeholder] || 'Search for a user',
+ null_user: opts[:null_user] || false,
+ any_user: opts[:any_user] || false,
+ email_user: opts[:email_user] || false,
+ first_user: opts[:first_user] && current_user ? current_user.username : false,
+ current_user: opts[:current_user] || false,
+ "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
+ author_id: opts[:author_id] || '',
+ skip_users: opts[:skip_users] ? opts[:skip_users].map(&:id) : nil,
+ }
+ end
end
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
new file mode 100644
index 00000000000..f8cccade15b
--- /dev/null
+++ b/app/helpers/sentry_helper.rb
@@ -0,0 +1,27 @@
+module SentryHelper
+ def sentry_enabled?
+ Rails.env.production? && current_application_settings.sentry_enabled?
+ end
+
+ def sentry_context
+ return unless sentry_enabled?
+
+ if current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
+
+ Raven.tags_context(program: sentry_program_context)
+ end
+
+ def sentry_program_context
+ if Sidekiq.server?
+ 'sidekiq'
+ else
+ 'rails'
+ end
+ end
+end
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
new file mode 100644
index 00000000000..2dd0bf5d71e
--- /dev/null
+++ b/app/helpers/services_helper.rb
@@ -0,0 +1,25 @@
+module ServicesHelper
+ def service_event_description(event)
+ case event
+ when "push"
+ "Event will be triggered by a push to the repository"
+ when "tag_push"
+ "Event will be triggered when a new tag is pushed to the repository"
+ when "note"
+ "Event will be triggered when someone adds a comment"
+ when "issue"
+ "Event will be triggered when an issue is created/updated/merged"
+ when "merge_request"
+ "Event will be triggered when a merge request is created/updated/merged"
+ when "build"
+ "Event will be triggered when a build status changes"
+ when "wiki_page"
+ "Event will be triggered when a wiki page is created/updated"
+ end
+ end
+
+ def service_event_field_name(event)
+ event = event.pluralize if %w[merge_request issue].include?(event)
+ "#{event}_events"
+ end
+end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index d86f1999f5c..8b138a8e69f 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -20,13 +20,19 @@ module SortingHelper
end
def projects_sort_options_hash
- {
+ options = {
sort_value_name => sort_title_name,
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,
}
+
+ if current_controller?('admin/projects')
+ options.merge!(sort_value_largest_repo => sort_title_largest_repo)
+ end
+
+ options
end
def sort_title_priority
@@ -102,11 +108,11 @@ module SortingHelper
end
def sort_value_oldest_created
- 'id_asc'
+ 'created_asc'
end
def sort_value_recently_created
- 'id_desc'
+ 'created_desc'
end
def sort_value_milestone_soon
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 8cb82c2d5cc..271e839692a 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -1,15 +1,6 @@
module TimeHelper
- def duration_in_words(finished_at, started_at)
- if finished_at && started_at
- interval_in_seconds = finished_at.to_i - started_at.to_i
- elsif started_at
- interval_in_seconds = Time.now.to_i - started_at.to_i
- end
-
- time_interval_in_words(interval_in_seconds)
- end
-
def time_interval_in_words(interval_in_seconds)
+ interval_in_seconds = interval_in_seconds.to_i
minutes = interval_in_seconds / 60
seconds = interval_in_seconds - minutes * 60
@@ -24,10 +15,9 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(finished_at, started_at)
- diff_in_seconds = finished_at.to_i - started_at.to_i
- time_format = diff_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration)
+ time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
- Time.at(diff_in_seconds).utc.strftime(time_format)
+ Time.at(duration).utc.strftime(time_format)
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index a832a6c8df7..0465327060e 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -1,10 +1,10 @@
module TodosHelper
def todos_pending_count
- TodosFinder.new(current_user, state: :pending).execute.count
+ @todos_pending_count ||= current_user.todos_pending_count
end
def todos_done_count
- TodosFinder.new(current_user, state: :done).execute.count
+ @todos_done_count ||= current_user.todos_done_count
end
def todo_action_name(todo)
@@ -13,6 +13,7 @@ module TodosHelper
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
when Todo::MARKED then 'added a todo for'
+ when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index dbedf417fa5..4a76c679bad 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -4,23 +4,11 @@ module TreeHelper
#
# contents - A Grit::Tree object for the current tree
def render_tree(tree)
- # Render Folders before Files/Submodules
+ # Sort submodules and folders together by name ahead of files
folders, files, submodules = tree.trees, tree.blobs, tree.submodules
-
tree = ""
-
- # Render folders if we have any
- tree << render(partial: 'projects/tree/tree_item', collection: folders,
- locals: { type: 'folder' }) if folders.present?
-
- # Render files if we have any
- tree << render(partial: 'projects/tree/blob_item', collection: files,
- locals: { type: 'file' }) if files.present?
-
- # Render submodules if we have any
- tree << render(partial: 'projects/tree/submodule_item',
- collection: submodules) if submodules.present?
-
+ items = (folders + submodules).sort_by(&:name) + files
+ tree << render(partial: "projects/tree/tree_row", collection: items) if items.present?
tree.html_safe
end
diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb
new file mode 100644
index 00000000000..143b4ca6b51
--- /dev/null
+++ b/app/helpers/u2f_helper.rb
@@ -0,0 +1,5 @@
+module U2fHelper
+ def inject_u2f_api?
+ browser.chrome? && browser.version.to_i >= 41 && !browser.device.mobile?
+ end
+end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 65598ad9ed3..d887cdadc34 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -28,4 +28,10 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_git_archive(repository, ref: ref, format: format))
head :ok
end
+
+ # Send an entry from artifacts through Workhorse
+ def send_artifacts_entry(build, entry)
+ headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
+ head :ok
+ end
end
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
index 2f86d1be576..3853af6201a 100644
--- a/app/mailers/emails/builds.rb
+++ b/app/mailers/emails/builds.rb
@@ -6,6 +6,7 @@ module Emails
add_project_headers
add_build_headers('failed')
+
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 6f54c42146c..d64e48f774b 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -6,6 +6,11 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
+ def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id)
+ setup_issue_mail(issue_id, recipient_id)
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
setup_issue_mail(issue_id, recipient_id)
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 9dd11d20ea6..ec27ac517db 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -6,6 +6,11 @@ module Emails
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id))
end
+ def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
+ end
+
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
setup_merge_request_mail(merge_request_id, recipient_id)
@@ -42,6 +47,13 @@ module Emails
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id))
end
+ def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
+ @resolved_by = User.find(resolved_by_user_id)
+ mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id))
+ end
+
private
def setup_merge_request_mail(merge_request_id, recipient_id)
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 236b6ab00d8..e0af7081411 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -29,7 +29,8 @@ module Emails
# used in notify layout
@target_url = @message.target_url
@project = Project.find(project_id)
-
+ @diff_notes_disabled = true
+
add_project_headers
headers['X-GitLab-Author'] = @message.author_username
diff --git a/app/models/ability.rb b/app/models/ability.rb
index eeb0ceba081..a49dd703926 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -6,6 +6,10 @@ class Ability
return [] unless user.is_a?(User)
return [] if user.blocked?
+ abilities_by_subject_class(user: user, subject: subject)
+ end
+
+ def abilities_by_subject_class(user:, subject:)
case subject
when CommitStatus then commit_status_abilities(user, subject)
when Project then project_abilities(user, subject)
@@ -47,6 +51,16 @@ class Ability
end
end
+ # Returns an Array of Issues that can be read by the given user.
+ #
+ # issues - The issues to reduce down to those readable by the user.
+ # user - The User for which to check the issues
+ def issues_readable_by_user(issues, user = nil)
+ return issues if user && user.admin?
+
+ issues.select { |issue| issue.visible_to_user?(user) }
+ end
+
# List of possible abilities for anonymous user
def anonymous_abilities(user, subject)
if subject.is_a?(PersonalSnippet)
@@ -76,6 +90,8 @@ class Ability
if project && project.public?
rules = [
:read_project,
+ :read_board,
+ :read_list,
:read_wiki,
:read_label,
:read_milestone,
@@ -150,38 +166,44 @@ class Ability
end
def project_abilities(user, project)
- rules = []
key = "/user/#{user.id}/project/#{project.id}"
- RequestStore.store[key] ||= begin
- # Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user))
+ if RequestStore.active?
+ RequestStore.store[key] ||= uncached_project_abilities(user, project)
+ else
+ uncached_project_abilities(user, project)
+ end
+ end
- owner = user.admin? ||
- project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ def uncached_project_abilities(user, project)
+ rules = []
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- if owner
- rules.push(*project_owner_rules)
- end
+ owner = user.admin? ||
+ project.owner == user ||
+ (project.group && project.group.has_owner?(user))
- if project.public? || (project.internal? && !user.external?)
- rules.push(*public_project_rules)
+ if owner
+ rules.push(*project_owner_rules)
+ end
- # Allow to read builds for internal projects
- rules << :read_build if project.public_builds?
+ if project.public? || (project.internal? && !user.external?)
+ rules.push(*public_project_rules)
- unless owner || project.team.member?(user) || project_group_member?(project, user)
- rules << :request_access
- end
- end
+ # Allow to read builds for internal projects
+ rules << :read_build if project.public_builds?
- if project.archived?
- rules -= project_archived_rules
+ unless owner || project.team.member?(user) || project_group_member?(project, user)
+ rules << :request_access if project.request_access_enabled
end
+ end
- rules - project_disabled_features_rules(project)
+ if project.archived?
+ rules -= project_archived_rules
end
+
+ (rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
@@ -204,7 +226,8 @@ class Ability
:download_code,
:fork_project,
:read_commit_status,
- :read_pipeline
+ :read_pipeline,
+ :read_container_image
]
end
@@ -213,6 +236,8 @@ class Ability
:read_project,
:read_wiki,
:read_issue,
+ :read_board,
+ :read_list,
:read_label,
:read_milestone,
:read_project_snippet,
@@ -234,6 +259,7 @@ class Ability
:update_issue,
:admin_issue,
:admin_label,
+ :admin_list,
:read_commit_status,
:read_build,
:read_container_image,
@@ -256,6 +282,7 @@ class Ability
:create_merge_request,
:create_wiki,
:push_code,
+ :resolve_note,
:create_container_image,
:update_container_image,
:create_environment,
@@ -372,7 +399,7 @@ class Ability
end
if group.public? || (group.internal? && !user.external?)
- rules << :request_access unless group.users.include?(user)
+ rules << :request_access if group.request_access_enabled && group.users.exclude?(user)
end
rules.flatten
@@ -387,6 +414,18 @@ class Ability
GroupProjectsFinder.new(group).execute(user).any?
end
+ def can_edit_note?(user, note)
+ return false if !note.editable? || !user.present?
+ return true if note.author == user || user.admin?
+
+ if note.project
+ max_access_level = note.project.team.max_member_access(user.id)
+ max_access_level >= Gitlab::Access::MASTER
+ else
+ false
+ end
+ end
+
def namespace_abilities(user, namespace)
rules = []
@@ -425,7 +464,8 @@ class Ability
rules += [
:read_note,
:update_note,
- :admin_note
+ :admin_note,
+ :resolve_note
]
end
@@ -433,6 +473,10 @@ class Ability
rules += project_abilities(user, note.project)
end
+ if note.for_merge_request? && note.noteable.author == user
+ rules << :resolve_note
+ end
+
rules
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index c6f77cc055f..246477ffe88 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -4,12 +4,20 @@ class ApplicationSetting < ActiveRecord::Base
add_authentication_token_field :health_check_access_token
CACHE_KEY = 'application_setting.last'
+ DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
+ | # or
+ \s # any whitespace character
+ | # or
+ [\r\n] # any number of newline characters
+ }x
serialize :restricted_visibility_levels
serialize :import_sources
serialize :disabled_oauth_sign_in_sources, Array
- serialize :restricted_signup_domains, Array
- attr_accessor :restricted_signup_domains_raw
+ serialize :domain_whitelist, Array
+ serialize :domain_blacklist, Array
+
+ attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay,
presence: true,
@@ -47,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
+ validates :koding_url,
+ presence: true,
+ if: :koding_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -62,6 +74,10 @@ class ApplicationSetting < ActiveRecord::Base
validates :enabled_git_access_protocol,
inclusion: { in: %w(ssh http), allow_blank: true, allow_nil: true }
+ validates :domain_blacklist,
+ presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
+ if: :domain_blacklist_enabled?
+
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
@@ -129,14 +145,16 @@ class ApplicationSetting < ActiveRecord::Base
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ domain_whitelist: Settings.gitlab['domain_whitelist'],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
+ koding_enabled: false,
+ koding_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
@@ -150,20 +168,30 @@ class ApplicationSetting < ActiveRecord::Base
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
- def restricted_signup_domains_raw
- self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil?
+ def domain_whitelist_raw
+ self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
+ end
+
+ def domain_blacklist_raw
+ self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
+ end
+
+ def domain_whitelist_raw=(values)
+ self.domain_whitelist = []
+ self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR)
+ self.domain_whitelist.reject! { |d| d.empty? }
+ self.domain_whitelist
+ end
+
+ def domain_blacklist_raw=(values)
+ self.domain_blacklist = []
+ self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR)
+ self.domain_blacklist.reject! { |d| d.empty? }
+ self.domain_blacklist
end
- def restricted_signup_domains_raw=(values)
- self.restricted_signup_domains = []
- self.restricted_signup_domains = values.split(
- /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
- | # or
- \s # any whitespace character
- | # or
- [\r\n] # any number of newline characters
- /x)
- self.restricted_signup_domains.reject! { |d| d.empty? }
+ def domain_blacklist_file=(file)
+ self.domain_blacklist_raw = file.read
end
def runners_registration_token
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 4279ea2ce57..12cc5aaafba 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -3,6 +3,9 @@ class Blob < SimpleDelegator
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
+ # The maximum size of an SVG that can be displayed.
+ MAXIMUM_SVG_SIZE = 2.megabytes
+
# Wrap a Gitlab::Git::Blob object, or return nil when given nil
#
# This method prevents the decorated object from evaluating to "truthy" when
@@ -31,6 +34,14 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
+ def size_within_svg_limits?
+ size <= MAXIMUM_SVG_SIZE
+ end
+
+ def video?
+ UploaderHelper::VIDEO_EXT.include?(extname.downcase.delete('.'))
+ end
+
def to_partial_path
if lfs_pointer?
'download'
diff --git a/app/models/board.rb b/app/models/board.rb
new file mode 100644
index 00000000000..3240c4bede3
--- /dev/null
+++ b/app/models/board.rb
@@ -0,0 +1,7 @@
+class Board < ActiveRecord::Base
+ belongs_to :project
+
+ has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
+
+ validates :project, presence: true
+end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e189dbac285..23c8de6f650 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -5,15 +5,18 @@ module Ci
belongs_to :erased_by, class_name: 'User'
serialize :options
+ serialize :yaml_variables
validates :coverage, numericality: true, allow_blank: true
validates_presence_of :ref
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
- scope :with_artifacts, ->() { where.not(artifacts_file: nil) }
+ scope :with_artifacts, ->() { where.not(artifacts_file: [nil, '']) }
+ scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
+ scope :manual_actions, ->() { where(when: :manual).relevant }
mount_uploader :artifacts_file, ArtifactUploader
mount_uploader :artifacts_metadata, ArtifactUploader
@@ -39,37 +42,36 @@ module Ci
end
def retry(build, user = nil)
- new_build = Ci::Build.new(status: 'pending')
- new_build.ref = build.ref
- new_build.tag = build.tag
- new_build.options = build.options
- new_build.commands = build.commands
- new_build.tag_list = build.tag_list
- new_build.project = build.project
- new_build.pipeline = build.pipeline
- new_build.name = build.name
- new_build.allow_failure = build.allow_failure
- new_build.stage = build.stage
- new_build.stage_idx = build.stage_idx
- new_build.trigger_request = build.trigger_request
- new_build.user = user
- new_build.save
+ new_build = Ci::Build.create(
+ ref: build.ref,
+ tag: build.tag,
+ options: build.options,
+ commands: build.commands,
+ tag_list: build.tag_list,
+ project: build.project,
+ pipeline: build.pipeline,
+ name: build.name,
+ allow_failure: build.allow_failure,
+ stage: build.stage,
+ stage_idx: build.stage_idx,
+ trigger_request: build.trigger_request,
+ yaml_variables: build.yaml_variables,
+ when: build.when,
+ user: user,
+ environment: build.environment,
+ status_event: 'enqueue'
+ )
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
- state_machine :status, initial: :pending do
+ state_machine :status do
after_transition pending: :running do |build|
build.execute_hooks
end
- # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed
- around_transition any => [:success, :failed, :canceled] do |build, block|
- block.call
- build.pipeline.create_next_builds(build) if build.pipeline
- end
-
after_transition any => [:success, :failed, :canceled] do |build|
build.update_coverage
build.execute_hooks
@@ -87,6 +89,29 @@ module Ci
end
end
+ def manual?
+ self.when == 'manual'
+ end
+
+ def other_actions
+ pipeline.manual_actions.where.not(name: name)
+ end
+
+ def playable?
+ project.builds_enabled? && commands.present? && manual? && skipped?
+ end
+
+ def play(current_user = nil)
+ # Try to queue a current build
+ if self.enqueue
+ self.update(user: current_user)
+ self
+ else
+ # Otherwise we need to create a duplicate
+ Ci::Build.retry(self, current_user)
+ end
+ end
+
def retryable?
project.builds_enabled? && commands.present? && complete?
end
@@ -117,7 +142,15 @@ module Ci
end
def variables
- predefined_variables + yaml_variables + project_variables + trigger_variables
+ variables = predefined_variables
+ variables += project.predefined_variables
+ variables += pipeline.predefined_variables
+ variables += runner.predefined_variables if runner
+ variables += project.container_registry_variables
+ variables += yaml_variables
+ variables += project.secret_variables
+ variables += trigger_request.user_variables if trigger_request
+ variables
end
def merge_request
@@ -295,7 +328,7 @@ module Ci
end
def valid_token?(token)
- project.valid_runners_token? token
+ project.valid_runners_token?(token)
end
def has_tags?
@@ -312,14 +345,14 @@ module Ci
def execute_hooks
return unless project
- build_data = Gitlab::BuildDataBuilder.build(self)
+ build_data = Gitlab::DataBuilder::Build.build(self)
project.execute_hooks(build_data.dup, :build_hooks)
project.execute_services(build_data.dup, :build_hooks)
project.running_or_pending_build_count(force: true)
end
def artifacts?
- !artifacts_expired? && artifacts_file.exists?
+ !artifacts_expired? && self[:artifacts_file].present?
end
def artifacts_metadata?
@@ -376,6 +409,14 @@ module Ci
self.update(artifacts_expire_at: nil)
end
+ def when
+ read_attribute(:when) || build_attributes_from_config[:when] || 'on_success'
+ end
+
+ def yaml_variables
+ read_attribute(:yaml_variables) || build_attributes_from_config[:yaml_variables] || []
+ end
+
private
def update_artifacts_size
@@ -394,53 +435,30 @@ module Ci
self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil)
end
- def yaml_variables
- global_yaml_variables + job_yaml_variables
- end
-
- def global_yaml_variables
- if pipeline.config_processor
- pipeline.config_processor.global_variables.map do |key, value|
- { key: key, value: value, public: true }
- end
- else
- []
- end
- end
-
- def job_yaml_variables
- if pipeline.config_processor
- pipeline.config_processor.job_variables(name).map do |key, value|
- { key: key, value: value, public: true }
- end
- else
- []
- end
- end
-
- def project_variables
- project.variables.map do |variable|
- { key: variable.key, value: variable.value, public: false }
- end
+ def predefined_variables
+ variables = [
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_BUILD_ID', value: id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: token, public: false },
+ { 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_NAME', value: name, public: true },
+ { key: 'CI_BUILD_STAGE', value: stage, public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
+ ]
+ variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
+ variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
+ variables
end
- def trigger_variables
- if trigger_request && trigger_request.variables
- trigger_request.variables.map do |key, value|
- { key: key, value: value, public: false }
- end
- else
- []
- end
- end
+ def build_attributes_from_config
+ return {} unless pipeline.config_processor
- def predefined_variables
- variables = []
- variables << { key: :CI_BUILD_TAG, value: ref, public: true } if tag?
- variables << { key: :CI_BUILD_NAME, value: name, public: true }
- variables << { key: :CI_BUILD_STAGE, value: stage, public: true }
- variables << { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } if trigger_request
- variables
+ pipeline.config_processor.build_attributes(name)
end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index fa4071e2482..03812cd195f 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,23 +1,74 @@
module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
- include Statuseable
+ include HasStatus
self.table_name = 'ci_commits'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
+ belongs_to :user
+
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id
validates_presence_of :sha
+ validates_presence_of :ref
validates_presence_of :status
validate :valid_commit_sha
- # Invalidate object and save if when touched
- after_touch :update_state
after_save :keep_around_commits
+ delegate :stages, to: :statuses
+
+ state_machine :status, initial: :created do
+ event :enqueue do
+ transition created: :pending
+ transition [:success, :failed, :canceled, :skipped] => :running
+ end
+
+ event :run do
+ transition any => :running
+ end
+
+ event :skip do
+ transition any => :skipped
+ end
+
+ event :drop do
+ transition any => :failed
+ end
+
+ event :succeed do
+ transition any => :success
+ end
+
+ event :cancel do
+ transition any => :canceled
+ end
+
+ before_transition [:created, :pending] => :running do |pipeline|
+ pipeline.started_at = Time.now
+ end
+
+ before_transition any => [:success, :failed, :canceled] do |pipeline|
+ pipeline.finished_at = Time.now
+ end
+
+ before_transition do |pipeline|
+ pipeline.update_duration
+ end
+
+ after_transition do |pipeline, transition|
+ pipeline.execute_hooks unless transition.loopback?
+ end
+ end
+
+ # ref can't be HEAD or SHA, can only be branch/tag name
+ scope :latest_successful_for, ->(ref = default_branch) do
+ where(ref: ref).success.order(id: :desc).limit(1)
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -27,6 +78,14 @@ module Ci
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)
+ end
+
def project_id
project.id
end
@@ -49,6 +108,10 @@ module Ci
commit.try(:message)
end
+ def git_commit_title
+ commit.try(:title)
+ end
+
def short_sha
Ci::Pipeline.truncate_sha(sha)
end
@@ -63,6 +126,10 @@ module Ci
!tag?
end
+ def manual_actions
+ builds.latest.manual_actions
+ end
+
def retryable?
builds.latest.any? do |build|
build.failed? && build.retryable?
@@ -83,6 +150,10 @@ module Ci
end
end
+ def mark_as_processable_after_stage(stage_idx)
+ builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ end
+
def latest?
return false unless ref
commit = project.commit(ref)
@@ -94,37 +165,6 @@ module Ci
trigger_requests.any?
end
- def create_builds(user, trigger_request = nil)
- ##
- # We persist pipeline only if there are builds available
- #
- return unless config_processor
-
- build_builds_for_stages(config_processor.stages, user,
- 'success', trigger_request) && save
- end
-
- def create_next_builds(build)
- return unless config_processor
-
- # don't create other builds if this one is retried
- latest_builds = builds.latest
- return unless latest_builds.exists?(build.id)
-
- # get list of stages after this build
- next_stages = config_processor.stages.drop_while { |stage| stage != build.stage }
- next_stages.delete(build.stage)
-
- # get status for all prior builds
- prior_builds = latest_builds.where.not(stage: next_stages)
- prior_status = prior_builds.status
-
- # build builds for next stage that has builds available
- # and save pipeline if we have builds
- build_builds_for_stages(next_stages, build.user, prior_status,
- build.trigger_request) && save
- end
-
def retried
@retried ||= (statuses.order(id: :desc) - statuses.latest)
end
@@ -136,6 +176,18 @@ module Ci
end
end
+ def config_builds_attributes
+ return [] unless config_processor
+
+ config_processor.
+ builds_for_ref(ref, tag?, trigger_requests.first).
+ sort_by { |build| build[:stage_idx] }
+ end
+
+ def has_warnings?
+ builds.latest.ignored.any?
+ end
+
def config_processor
return nil unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@@ -163,10 +215,6 @@ module Ci
end
end
- def skip_ci?
- git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message
- end
-
def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq
end
@@ -188,33 +236,52 @@ module Ci
Note.for_commit_id(sha)
end
- private
+ def process!
+ Ci::ProcessPipelineService.new(project, user).execute(self)
+ end
- def build_builds_for_stages(stages, user, status, trigger_request)
- ##
- # Note that `Array#any?` implements a short circuit evaluation, so we
- # build builds only for the first stage that has builds available.
- #
- stages.any? do |stage|
- CreateBuildsService.new(self)
- .execute(stage, user, status, trigger_request).present?
+ def build_updated
+ case latest_builds_status
+ when 'pending' then enqueue
+ when 'running' then run
+ when 'success' then succeed
+ when 'failed' then drop
+ when 'canceled' then cancel
+ when 'skipped' then skip
end
end
- def update_state
- statuses.reload
- self.status = if yaml_errors.blank?
- statuses.latest.status || 'skipped'
- else
- 'failed'
- end
- self.started_at = statuses.started_at
- self.finished_at = statuses.finished_at
- self.duration = statuses.latest.duration
- save
+ def predefined_variables
+ [
+ { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
+ ]
+ end
+
+ def update_duration
+ self.duration = calculate_duration
+ end
+
+ def execute_hooks
+ data = pipeline_data
+ project.execute_hooks(data, :pipeline_hooks)
+ project.execute_services(data, :pipeline_hooks)
+ end
+
+ private
+
+ def pipeline_data
+ Gitlab::DataBuilder::Pipeline.build(self)
+ end
+
+ def latest_builds_status
+ return 'failed' unless yaml_errors.blank?
+
+ statuses.latest.status || 'skipped'
end
def keep_around_commits
+ return unless project
+
project.repository.keep_around(self.sha)
project.repository.keep_around(self.before_sha)
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index b64ec79ec2b..49f05f881a2 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -114,6 +114,14 @@ module Ci
tag_list.any?
end
+ def predefined_variables
+ [
+ { key: 'CI_RUNNER_ID', value: id.to_s, public: true },
+ { key: 'CI_RUNNER_DESCRIPTION', value: description, public: true },
+ { key: 'CI_RUNNER_TAGS', value: tag_list.to_s, public: true }
+ ]
+ end
+
private
def tag_constraints
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index fcf2b6dc5e2..fc674871743 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -7,5 +7,13 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
serialize :variables
+
+ def user_variables
+ return [] unless variables
+
+ variables.map do |key, value|
+ { key: key, value: value, public: false }
+ end
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 2ef3973c160..817d063e4a2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -104,7 +104,7 @@ class Commit
end
def diff_line_count
- @diff_line_count ||= Commit::diff_line_count(self.diffs)
+ @diff_line_count ||= Commit::diff_line_count(raw_diffs)
@diff_line_count
end
@@ -123,15 +123,17 @@ class Commit
# In case this first line is longer than 100 characters, it is cut off
# after 80 characters and ellipses (`&hellp;`) are appended.
def title
- title = safe_message
+ full_title.length > 100 ? full_title[0..79] << "…" : full_title
+ end
- return no_commit_message if title.blank?
+ # Returns the full commits title
+ def full_title
+ return @full_title if @full_title
- title_end = title.index("\n")
- if (!title_end && title.length > 100) || (title_end && title_end > 100)
- title[0..79] << "…"
+ if safe_message.blank?
+ @full_title = no_commit_message
else
- title.split("\n", 2).first
+ @full_title = safe_message.split("\n", 2).first
end
end
@@ -178,7 +180,18 @@ class Commit
end
def author
- @author ||= User.find_by_any_email(author_email.downcase)
+ if RequestStore.active?
+ key = "commit_author:#{author_email.downcase}"
+ # nil is a valid value since no author may exist in the system
+ if RequestStore.store.has_key?(key)
+ @author = RequestStore.store[key]
+ else
+ @author = find_author_by_any_email
+ RequestStore.store[key] = @author
+ end
+ else
+ @author ||= find_author_by_any_email
+ end
end
def committer
@@ -216,7 +229,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: self.parent_id || self.sha,
+ base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
head_sha: self.sha
)
end
@@ -295,8 +308,8 @@ class Commit
def uri_type(path)
entry = @raw.tree.path(path)
if entry[:type] == :blob
- blob = Gitlab::Git::Blob.new(name: entry[:name])
- blob.image? ? :raw : :blob
+ blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
+ blob.image? || blob.video? ? :raw : :blob
else
entry[:type]
end
@@ -304,12 +317,24 @@ class Commit
nil
end
+ def raw_diffs(*args)
+ raw.diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
+ end
+
private
+ def find_author_by_any_email
+ User.find_by_any_email(author_email.downcase)
+ end
+
def repo_changes
changes = { added: [], modified: [], removed: [] }
- diffs.each do |diff|
+ raw_diffs(deltas_only: true).each do |diff|
if diff.deleted_file
changes[:removed] << diff.old_path
elsif diff.renamed_file || diff.new_file
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e437e3417a8..4a628924499 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,11 +1,11 @@
class CommitStatus < ActiveRecord::Base
- include Statuseable
+ include HasStatus
include Importable
self.table_name = 'ci_builds'
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
+ belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
delegate :commit, to: :pipeline
@@ -16,29 +16,52 @@ class CommitStatus < ActiveRecord::Base
alias_attribute :author, :user
- scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :commit_id)) }
+ scope :latest, -> do
+ max_id = unscope(:select).select("max(#{quoted_table_name}.id)")
+
+ where(id: max_id.group(:name, :commit_id))
+ end
+
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+ scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
+
+ state_machine :status do
+ event :enqueue do
+ transition [:created, :skipped] => :pending
+ end
+
+ event :process do
+ transition skipped: :created
+ end
- state_machine :status, initial: :pending do
event :run do
transition pending: :running
end
+ event :skip do
+ transition [:created, :pending] => :skipped
+ end
+
event :drop do
- transition [:pending, :running] => :failed
+ transition [:created, :pending, :running] => :failed
end
event :success do
- transition [:pending, :running] => :success
+ transition [:created, :pending, :running] => :success
end
event :cancel do
- transition [:pending, :running] => :canceled
+ transition [:created, :pending, :running] => :canceled
+ end
+
+ after_transition created: [:pending, :running] do |commit_status|
+ commit_status.update_attributes queued_at: Time.now
end
- after_transition pending: :running do |commit_status|
+ after_transition [:created, :pending] => :running do |commit_status|
commit_status.update_attributes started_at: Time.now
end
@@ -46,7 +69,18 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- after_transition [:pending, :running] => :success do |commit_status|
+ # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
+ around_transition any => [:success, :failed, :canceled] do |commit_status, block|
+ block.call
+
+ commit_status.pipeline.try(:process!)
+ end
+
+ after_transition do |commit_status, transition|
+ commit_status.pipeline.try(:build_updated) unless transition.loopback?
+ end
+
+ after_transition [:created, :pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end
@@ -80,13 +114,7 @@ class CommitStatus < ActiveRecord::Base
end
def duration
- duration =
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
- duration
+ calculate_duration
end
def stuck?
diff --git a/app/models/compare.rb b/app/models/compare.rb
new file mode 100644
index 00000000000..4856510f526
--- /dev/null
+++ b/app/models/compare.rb
@@ -0,0 +1,66 @@
+class Compare
+ delegate :same, :head, :base, to: :@compare
+
+ attr_reader :project
+
+ def self.decorate(compare, project)
+ if compare.is_a?(Compare)
+ compare
+ else
+ self.new(compare, project)
+ end
+ end
+
+ def initialize(compare, project)
+ @compare = compare
+ @project = project
+ end
+
+ def commits
+ @commits ||= Commit.decorate(@compare.commits, project)
+ end
+
+ def start_commit
+ return @start_commit if defined?(@start_commit)
+
+ commit = @compare.base
+ @start_commit = commit ? ::Commit.new(commit, project) : nil
+ end
+
+ def head_commit
+ return @head_commit if defined?(@head_commit)
+
+ commit = @compare.head
+ @head_commit = commit ? ::Commit.new(commit, project) : nil
+ end
+ alias_method :commit, :head_commit
+
+ def base_commit
+ return @base_commit if defined?(@base_commit)
+
+ @base_commit = if start_commit && head_commit
+ project.merge_base_commit(start_commit.id, head_commit.id)
+ else
+ nil
+ end
+ end
+
+ def raw_diffs(*args)
+ @compare.diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::Compare.new(self,
+ project: project,
+ diff_options: diff_options,
+ diff_refs: diff_refs)
+ end
+
+ def diff_refs
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: base_commit.try(:sha),
+ start_sha: start_commit.try(:sha),
+ head_sha: commit.try(:sha)
+ )
+ end
+end
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 06beff177b1..800a16ab246 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -65,8 +65,7 @@ module Awardable
def create_award_emoji(name, current_user)
return unless emoji_awardable?
-
- award_emoji.create(name: name, user: current_user)
+ award_emoji.create(name: normalize_name(name), user: current_user)
end
def remove_award_emoji(name, current_user)
@@ -80,4 +79,10 @@ module Awardable
create_award_emoji(emoji_name, current_user)
end
end
+
+ private
+
+ def normalize_name(name)
+ Gitlab::AwardEmoji.normalize_emoji_name(name)
+ end
end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 00000000000..be93435453b
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
+ end
+
+ def expires?
+ expires_at.present?
+ end
+
+ def expires_soon?
+ expires_at < 7.days.from_now
+ end
+end
diff --git a/app/models/concerns/faster_cache_keys.rb b/app/models/concerns/faster_cache_keys.rb
new file mode 100644
index 00000000000..5b14723fa2d
--- /dev/null
+++ b/app/models/concerns/faster_cache_keys.rb
@@ -0,0 +1,16 @@
+module FasterCacheKeys
+ # A faster version of Rails' "cache_key" method.
+ #
+ # Rails' default "cache_key" method uses all kind of complex logic to figure
+ # out the cache key. In many cases this complexity and overhead may not be
+ # needed.
+ #
+ # This method does not do any timestamp parsing as this process is quite
+ # expensive and not needed when generating cache keys. This method also relies
+ # on the table name instead of the cache namespace name as the latter uses
+ # complex logic to generate the exact same value (as when using the table
+ # name) in 99% of the cases.
+ def cache_key
+ "#{self.class.table_name}/#{id}-#{read_attribute_before_type_cast(:updated_at)}"
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
new file mode 100644
index 00000000000..f7b8352405c
--- /dev/null
+++ b/app/models/concerns/has_status.rb
@@ -0,0 +1,93 @@
+module HasStatus
+ extend ActiveSupport::Concern
+
+ 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]
+
+ class_methods do
+ def status_sql
+ scope = all.relevant
+ builds = scope.select('count(*)').to_sql
+ success = scope.success.select('count(*)').to_sql
+ ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
+ ignored ||= '0'
+ pending = scope.pending.select('count(*)').to_sql
+ running = scope.running.select('count(*)').to_sql
+ canceled = scope.canceled.select('count(*)').to_sql
+ skipped = scope.skipped.select('count(*)').to_sql
+
+ deduce_status = "(CASE
+ WHEN (#{builds})=0 THEN NULL
+ WHEN (#{builds})=(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
+ WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
+ WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
+ WHEN (#{running})+(#{pending})>0 THEN 'running'
+ ELSE 'failed'
+ END)"
+
+ deduce_status
+ end
+
+ def status
+ all.pluck(self.status_sql).first
+ end
+
+ def started_at
+ all.minimum(:started_at)
+ end
+
+ def finished_at
+ all.maximum(:finished_at)
+ end
+ end
+
+ included do
+ validates :status, inclusion: { in: AVAILABLE_STATUSES }
+
+ state_machine :status, initial: :created do
+ state :created, value: 'created'
+ state :pending, value: 'pending'
+ state :running, value: 'running'
+ state :failed, value: 'failed'
+ state :success, value: 'success'
+ state :canceled, value: 'canceled'
+ state :skipped, value: 'skipped'
+ end
+
+ scope :created, -> { where(status: 'created') }
+ scope :relevant, -> { where.not(status: 'created') }
+ scope :running, -> { where(status: 'running') }
+ scope :pending, -> { where(status: 'pending') }
+ scope :success, -> { where(status: 'success') }
+ scope :failed, -> { where(status: 'failed') }
+ scope :canceled, -> { where(status: 'canceled') }
+ scope :skipped, -> { where(status: 'skipped') }
+ scope :running_or_pending, -> { where(status: [:running, :pending]) }
+ scope :finished, -> { where(status: [:success, :failed, :canceled]) }
+ end
+
+ def started?
+ STARTED_STATUSES.include?(status) && started_at
+ end
+
+ def active?
+ ACTIVE_STATUSES.include?(status)
+ end
+
+ def complete?
+ COMPLETED_STATUSES.include?(status)
+ end
+
+ private
+
+ def calculate_duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index acb6f5a2998..8e11d4f57cf 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -17,7 +17,7 @@ module Issuable
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :milestone
- has_many :notes, as: :noteable, dependent: :destroy do
+ has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :destroy do
def authors_loaded?
# We check first if we're loaded to not load unnecessarily.
loaded? && to_a.all? { |note| note.association(:author).loaded? }
@@ -87,6 +87,12 @@ module Issuable
User.find(assignee_id_was).update_cache_counts if assignee_id_was
assignee.update_cache_counts if assignee
end
+
+ # We want to use optimistic lock for cases when only title or description are involved
+ # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
+ def locking_enabled?
+ title_changed? || description_changed?
+ end
end
module ClassMethods
@@ -131,7 +137,10 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+ condition_field = "#{table_name}.id"
+ highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -159,20 +168,6 @@ module Issuable
grouping_columns
end
-
- private
-
- def highest_label_priority(excluded_labels)
- query = Label.select(Label.arel_table[:priority].minimum).
- joins(:label_links).
- where(label_links: { target_type: name }).
- where("label_links.target_id = #{table_name}.id").
- reorder(nil)
-
- query.where.not(title: excluded_labels) if excluded_labels.present?
-
- query
- end
end
def today?
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 8cac47246db..ec9e0f1b1d0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -14,14 +14,14 @@ module Mentionable
attr = attr.to_s
mentionable_attrs << [attr, options]
end
+ end
+ included do
# Accessor for attributes marked mentionable.
- def mentionable_attrs
- @mentionable_attrs ||= []
+ cattr_accessor :mentionable_attrs, instance_accessor: false do
+ []
end
- end
- included do
if self < Participable
participant -> (user, ext) { all_references(user, extractor: ext) }
end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 2785fbb21c9..a881fb83b7f 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -1,12 +1,6 @@
module NoteOnDiff
extend ActiveSupport::Concern
- NUMBER_OF_TRUNCATED_DIFF_LINES = 16
-
- included do
- delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
- end
-
def diff_note?
true
end
@@ -23,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
+ def original_line_code
+ raise NotImplementedError
+ end
+
def diff_attributes
raise NotImplementedError
end
@@ -30,23 +28,4 @@ module NoteOnDiff
def can_be_award_emoji?
false
end
-
- # Returns an array of at most 16 highlighted lines above a diff note
- def truncated_diff_lines
- prev_lines = []
-
- highlighted_diff_lines.each do |line|
- if line.meta?
- prev_lines.clear
- else
- prev_lines << line
-
- break if for_line?(line)
-
- prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
- end
- end
-
- prev_lines
- end
end
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 9822844357d..70740c76e43 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -41,9 +41,12 @@ module Participable
def participant(attr)
participant_attrs << attr
end
+ end
- def participant_attrs
- @participant_attrs ||= []
+ included do
+ # Accessor for participant attributes.
+ cattr_accessor :participant_attrs, instance_accessor: false do
+ []
end
end
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
new file mode 100644
index 00000000000..5a7b36070e7
--- /dev/null
+++ b/app/models/concerns/protected_branch_access.rb
@@ -0,0 +1,7 @@
+module ProtectedBranchAccess
+ extend ActiveSupport::Concern
+
+ def humanize
+ self.class.human_access_levels[self.access_level]
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd..1ebecd86af9 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,19 @@ module Sortable
all
end
end
+
+ private
+
+ def highest_label_priority(object_types, condition_field, excluded_labels: [])
+ query = Label.select(Label.arel_table[:priority].minimum).
+ joins(:label_links).
+ where(label_links: { target_type: object_types }).
+ where("label_links.target_id = #{condition_field}").
+ reorder(nil)
+
+ query.where.not(title: excluded_labels) if excluded_labels.present?
+
+ query
+ end
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
new file mode 100644
index 00000000000..1aa97debe42
--- /dev/null
+++ b/app/models/concerns/spammable.rb
@@ -0,0 +1,68 @@
+module Spammable
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def attr_spammable(attr, options = {})
+ spammable_attrs << [attr.to_s, options]
+ end
+ end
+
+ included do
+ has_one :user_agent_detail, as: :subject, dependent: :destroy
+
+ attr_accessor :spam
+
+ after_validation :check_for_spam, on: :create
+
+ cattr_accessor :spammable_attrs, instance_accessor: false do
+ []
+ end
+
+ delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
+ end
+
+ def submittable_as_spam?
+ if user_agent_detail
+ user_agent_detail.submittable? && current_application_settings.akismet_enabled
+ else
+ false
+ end
+ end
+
+ def spam?
+ @spam
+ end
+
+ def check_for_spam
+ self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
+ end
+
+ def spam_title
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_title, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spam_description
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_description, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spammable_text
+ result = self.class.spammable_attrs.map do |attr|
+ public_send(attr.first)
+ end
+
+ result.reject(&:blank?).join("\n")
+ end
+
+ # Override in Spammable if further checks are necessary
+ def check_for_spam?
+ true
+ end
+end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
deleted file mode 100644
index 3ef91caad47..00000000000
--- a/app/models/concerns/statuseable.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-module Statuseable
- extend ActiveSupport::Concern
-
- AVAILABLE_STATUSES = %w(pending running success failed canceled skipped)
-
- class_methods do
- def status_sql
- builds = all.select('count(*)').to_sql
- success = all.success.select('count(*)').to_sql
- ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored)
- ignored ||= '0'
- pending = all.pending.select('count(*)').to_sql
- running = all.running.select('count(*)').to_sql
- canceled = all.canceled.select('count(*)').to_sql
- skipped = all.skipped.select('count(*)').to_sql
-
- deduce_status = "(CASE
- WHEN (#{builds})=0 THEN NULL
- WHEN (#{builds})=(#{success})+(#{ignored}) THEN 'success'
- WHEN (#{builds})=(#{pending}) THEN 'pending'
- WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored}) THEN 'canceled'
- WHEN (#{builds})=(#{skipped}) THEN 'skipped'
- WHEN (#{running})+(#{pending})>0 THEN 'running'
- ELSE 'failed'
- END)"
-
- deduce_status
- end
-
- def status
- all.pluck(self.status_sql).first
- end
-
- def duration
- duration_array = all.map(&:duration).compact
- duration_array.reduce(:+)
- end
-
- def started_at
- all.minimum(:started_at)
- end
-
- def finished_at
- all.maximum(:finished_at)
- end
- end
-
- included do
- validates :status, inclusion: { in: AVAILABLE_STATUSES }
-
- state_machine :status, initial: :pending do
- state :pending, value: 'pending'
- state :running, value: 'running'
- state :failed, value: 'failed'
- state :success, value: 'success'
- state :canceled, value: 'canceled'
- state :skipped, value: 'skipped'
- end
-
- scope :running, -> { where(status: 'running') }
- scope :pending, -> { where(status: 'pending') }
- scope :success, -> { where(status: 'success') }
- scope :failed, -> { where(status: 'failed') }
- scope :canceled, -> { where(status: 'canceled') }
- scope :skipped, -> { where(status: 'skipped') }
- scope :running_or_pending, -> { where(status: [:running, :pending]) }
- scope :finished, -> { where(status: [:success, :failed, :canceled]) }
- end
-
- def started?
- !pending? && !canceled? && started_at
- end
-
- def active?
- running? || pending?
- end
-
- def complete?
- canceled? || success? || failed?
- end
-end
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 885deaf78d2..24c7b26d223 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -1,12 +1,26 @@
module TokenAuthenticatable
extend ActiveSupport::Concern
+ private
+
+ def write_new_token(token_field)
+ new_token = generate_token(token_field)
+ write_attribute(token_field, new_token)
+ end
+
+ def generate_token(token_field)
+ loop do
+ token = Devise.friendly_token
+ break token unless self.class.unscoped.find_by(token_field => token)
+ end
+ end
+
class_methods do
def authentication_token_fields
@token_fields || []
end
- private
+ private # rubocop:disable Lint/UselessAccessModifier
def add_authentication_token_field(token_field)
@token_fields = [] unless @token_fields
@@ -32,18 +46,4 @@ module TokenAuthenticatable
end
end
end
-
- private
-
- def write_new_token(token_field)
- new_token = generate_token(token_field)
- write_attribute(token_field, new_token)
- end
-
- def generate_token(token_field)
- loop do
- token = Devise.friendly_token
- break token unless self.class.unscoped.find_by(token_field => token)
- end
- end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 520026c18dd..1e338889714 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -32,4 +32,14 @@ class Deployment < ActiveRecord::Base
def keep_around_commit
project.repository.keep_around(self.sha)
end
+
+ def manual_actions
+ deployable.try(:other_actions)
+ end
+
+ def includes_commit?(commit)
+ return false unless commit
+
+ project.repository.is_ancestor?(commit.id, sha)
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 9671955db36..c8320ff87fa 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -9,11 +9,16 @@ class DiffNote < Note
validates :diff_line, presence: true
validates :line_code, presence: true, line_code: true
validates :noteable_type, inclusion: { in: ['Commit', 'MergeRequest'] }
+ validates :resolved_by, presence: true, if: :resolved?
validate :positions_complete
validate :verify_supported
+ after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
- before_validation :set_line_code
+ before_validation :set_line_code, :set_original_discussion_id
+ # We need to do this again, because it's already in `Note`, but is affected by
+ # `update_position` and needs to run after that.
+ before_validation :set_discussion_id
after_save :keep_around_commits
class << self
@@ -30,14 +35,6 @@ class DiffNote < Note
{ position: position.to_json }
end
- def discussion_id
- @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
- end
-
- def original_discussion_id
- @original_discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
- end
-
def position=(new_position)
if new_position.is_a?(String)
new_position = JSON.parse(new_position) rescue nil
@@ -63,19 +60,69 @@ class DiffNote < Note
diff_file.position(line) == self.original_position
end
+ def original_line_code
+ self.diff_file.line_code(self.diff_line)
+ end
+
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
- diff_refs ||= self.noteable.diff_refs
+ diff_refs ||= noteable_diff_refs
self.position.diff_refs == diff_refs
end
+ def resolvable?
+ !system? && for_merge_request?
+ end
+
+ def resolved?
+ return false unless resolvable?
+
+ self.resolved_at.present?
+ end
+
+ def resolve!(current_user)
+ return unless resolvable?
+ return if resolved?
+
+ self.resolved_at = Time.now
+ self.resolved_by = current_user
+ save!
+ end
+
+ def unresolve!
+ return unless resolvable?
+ return unless resolved?
+
+ self.resolved_at = nil
+ self.resolved_by = nil
+ save!
+ end
+
+ def discussion
+ return unless resolvable?
+
+ self.noteable.find_diff_discussion(self.discussion_id)
+ end
+
+ def to_discussion
+ Discussion.new([self])
+ end
+
private
def supported?
- !self.for_merge_request? || self.noteable.support_new_diff_notes?
+ for_commit? || self.noteable.has_complete_diff_refs?
+ end
+
+ def noteable_diff_refs
+ if noteable.respond_to?(:diff_sha_refs)
+ noteable.diff_sha_refs
+ else
+ noteable.diff_refs
+ end
end
def set_original_position
@@ -86,6 +133,26 @@ class DiffNote < Note
self.line_code = self.position.line_code(self.project.repository)
end
+ def ensure_original_discussion_id
+ return unless self.persisted?
+ return if self.original_discussion_id
+
+ set_original_discussion_id
+ update_column(:original_discussion_id, self.original_discussion_id)
+ end
+
+ def set_original_discussion_id
+ self.original_discussion_id = Digest::SHA1.hexdigest(build_original_discussion_id)
+ end
+
+ def build_discussion_id
+ self.class.build_discussion_id(noteable_type, noteable_id || commit_id, position)
+ end
+
+ def build_original_discussion_id
+ self.class.build_discussion_id(noteable_type, noteable_id || commit_id, original_position)
+ end
+
def update_position
return unless supported?
return if for_commit?
@@ -96,7 +163,7 @@ class DiffNote < Note
self.project,
nil,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: self.noteable.diff_refs,
+ new_diff_refs: noteable_diff_refs,
paths: self.position.paths
).execute(self)
end
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
new file mode 100644
index 00000000000..9676bc03470
--- /dev/null
+++ b/app/models/discussion.rb
@@ -0,0 +1,177 @@
+class Discussion
+ NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+
+ attr_reader :first_note, :last_note, :notes
+
+ delegate :created_at,
+ :project,
+ :author,
+
+ :noteable,
+ :for_commit?,
+ :for_merge_request?,
+
+ :line_code,
+ :original_line_code,
+ :diff_file,
+ :for_line?,
+ :active?,
+
+ to: :first_note
+
+ delegate :resolved_at,
+ :resolved_by,
+
+ to: :last_resolved_note,
+ allow_nil: true
+
+ delegate :blob, :highlighted_diff_lines, to: :diff_file, allow_nil: true
+
+ def self.for_notes(notes)
+ notes.group_by(&:discussion_id).values.map { |notes| new(notes) }
+ end
+
+ def self.for_diff_notes(notes)
+ notes.group_by(&:line_code).values.map { |notes| new(notes) }
+ end
+
+ def initialize(notes)
+ @first_note = notes.first
+ @last_note = notes.last
+ @notes = notes
+ end
+
+ def last_resolved_note
+ return unless resolved?
+
+ @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last
+ end
+
+ def last_updated_at
+ last_note.created_at
+ end
+
+ def last_updated_by
+ last_note.author
+ end
+
+ def id
+ first_note.discussion_id
+ end
+
+ alias_method :to_param, :id
+
+ def diff_discussion?
+ first_note.diff_note?
+ end
+
+ def legacy_diff_discussion?
+ notes.any?(&:legacy_diff_note?)
+ end
+
+ def resolvable?
+ return @resolvable if defined?(@resolvable)
+
+ @resolvable = diff_discussion? && notes.any?(&:resolvable?)
+ end
+
+ def resolved?
+ return @resolved if defined?(@resolved)
+
+ @resolved = resolvable? && notes.none?(&:to_be_resolved?)
+ end
+
+ def resolved_notes
+ notes.select(&:resolved?)
+ end
+
+ def to_be_resolved?
+ resolvable? && !resolved?
+ end
+
+ def can_resolve?(current_user)
+ return false unless current_user
+ return false unless resolvable?
+
+ current_user == self.noteable.author ||
+ current_user.can?(:resolve_note, self.project)
+ end
+
+ def resolve!(current_user)
+ return unless resolvable?
+
+ notes.each do |note|
+ note.resolve!(current_user) if note.resolvable?
+ end
+ end
+
+ def unresolve!
+ return unless resolvable?
+
+ notes.each do |note|
+ note.unresolve! if note.resolvable?
+ end
+ end
+
+ def for_target?(target)
+ self.noteable == target && !diff_discussion?
+ end
+
+ def active?
+ return @active if defined?(@active)
+
+ @active = first_note.active?
+ end
+
+ def collapsed?
+ return false unless diff_discussion?
+
+ if resolvable?
+ # New diff discussions only disappear once they are marked resolved
+ resolved?
+ else
+ # Old diff discussions disappear once they become outdated
+ !active?
+ end
+ end
+
+ def expanded?
+ !collapsed?
+ end
+
+ def reply_attributes
+ data = {
+ noteable_type: first_note.noteable_type,
+ noteable_id: first_note.noteable_id,
+ commit_id: first_note.commit_id,
+ discussion_id: self.id,
+ }
+
+ if diff_discussion?
+ data[:note_type] = first_note.type
+
+ data.merge!(first_note.diff_attributes)
+ end
+
+ data
+ end
+
+ # Returns an array of at most 16 highlighted lines above a diff note
+ def truncated_diff_lines
+ prev_lines = []
+
+ highlighted_diff_lines.each do |line|
+ if line.meta?
+ prev_lines.clear
+ else
+ prev_lines << line
+
+ break if for_line?(line)
+
+ prev_lines.shift if prev_lines.length >= NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ prev_lines
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index ac3a571a1f3..75e6f869786 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -3,6 +3,8 @@ class Environment < ActiveRecord::Base
has_many :deployments
+ before_validation :nullify_external_url
+
validates :name,
presence: true,
uniqueness: { scope: :project_id },
@@ -10,7 +12,23 @@ class Environment < ActiveRecord::Base
format: { with: Gitlab::Regex.environment_name_regex,
message: Gitlab::Regex.environment_name_regex_message }
+ validates :external_url,
+ uniqueness: { scope: :project_id },
+ length: { maximum: 255 },
+ allow_nil: true,
+ addressable_url: true
+
def last_deployment
deployments.last
end
+
+ def nullify_external_url
+ self.external_url = nil if self.external_url.blank?
+ end
+
+ def includes_commit?(commit)
+ return false unless last_deployment
+
+ last_deployment.includes_commit?(commit)
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b99701..c48869ae465 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user = nil)
+ def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
- Member.add_user(self.group_members, user_id, access_level, current_user)
+ Member.add_user(
+ self.group_members,
+ user_id,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
- def add_user(user, access_level, current_user = nil)
- add_users([user], access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ add_users([user], access_level, current_user: current_user, expires_at: expires_at)
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user)
+ add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user)
+ add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user)
+ add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user)
+ add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user)
+ add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index ba42a8eeb70..836a75b0608 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -5,5 +5,6 @@ class ProjectHook < WebHook
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
+ scope :pipeline_hooks, -> { where(pipeline_events: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 8b87b6c3d64..f365dee3141 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
default_value_for :build_events, false
+ default_value_for :pipeline_events, false
default_value_for :enable_ssl_verification, true
scope :push_hooks, -> { where(push_events: true) }
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 60abd47409e..788611305fe 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -6,6 +6,8 @@ class Issue < ActiveRecord::Base
include Referable
include Sortable
include Taskable
+ include Spammable
+ include FasterCacheKeys
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
@@ -34,6 +36,9 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ attr_spammable :title, spam_title: true
+ attr_spammable :description, spam_description: true
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -52,10 +57,50 @@ 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
@@ -189,7 +234,40 @@ class Issue < ActiveRecord::Base
self.closed_by_merge_requests(current_user).empty?
end
+ # Returns `true` if the current issue can be viewed by either a logged in User
+ # or an anonymous user.
+ def visible_to_user?(user = nil)
+ user ? readable_by?(user) : publicly_visible?
+ end
+
+ # Returns `true` if the given User can read the current Issue.
+ def readable_by?(user)
+ if user.admin?
+ true
+ elsif project.owner == user
+ true
+ elsif confidential?
+ author == user ||
+ assignee == user ||
+ project.team.member?(user, Gitlab::Access::REPORTER)
+ else
+ project.public? ||
+ project.internal? && !user.external? ||
+ project.team.member?(user)
+ end
+ end
+
+ # Returns `true` if this Issue is visible to everybody.
+ def publicly_visible?
+ project.public? && !confidential?
+ end
+
def overdue?
due_date.try(:past?) || false
end
+
+ # Only issues on public projects should be checked for spam
+ def check_for_spam?
+ project.public?
+ end
end
diff --git a/app/models/key.rb b/app/models/key.rb
index b9bc38a0436..568a60b8af3 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -26,8 +26,9 @@ class Key < ActiveRecord::Base
end
def publishable_key
- # Removes anything beyond the keytype and key itself
- self.key.split[0..1].join(' ')
+ # Strip out the keys comment so we don't leak email addresses
+ # Replace with simple ident of user_name (hostname)
+ self.key.split[0..1].push("#{self.user_name} (#{Gitlab.config.gitlab.host})").join(' ')
end
# projects that has this key
diff --git a/app/models/label.rb b/app/models/label.rb
index dc5586f5756..a23140b7d64 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -13,6 +13,8 @@ class Label < ActiveRecord::Base
default_value_for :color, DEFAULT_COLOR
belongs_to :project
+
+ has_many :lists, dependent: :destroy
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
@@ -52,14 +54,17 @@ class Label < ActiveRecord::Base
# This pattern supports cross-project references.
#
def self.reference_pattern
+ # NOTE: The id pattern only matches when all characters on the expression
+ # are digits, so it will match ~2 but not ~2fa because that's probably a
+ # label name and we want it to be matched as such.
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}
(?:
- (?<label_id>\d+) | # Integer-based label ID, or
+ (?<label_id>\d+(?!\S\w)\b) | # Integer-based label ID, or
(?<label_name>
- [A-Za-z0-9_\-\?&]+ | # String-based single-word label title, or
- "[^,]+" # String-based multi-word label surrounded in quotes
+ [A-Za-z0-9_\-\?\.&]+ | # String-based single-word label title, or
+ ".+?" # String-based multi-word label surrounded in quotes
)
)
}x
diff --git a/app/models/label_link.rb b/app/models/label_link.rb
index 47bd6eaf35f..51b5c2b1f4c 100644
--- a/app/models/label_link.rb
+++ b/app/models/label_link.rb
@@ -1,7 +1,9 @@
class LabelLink < ActiveRecord::Base
+ include Importable
+
belongs_to :target, polymorphic: true
belongs_to :label
- validates :target, presence: true
- validates :label, presence: true
+ validates :target, presence: true, unless: :importing?
+ validates :label, presence: true, unless: :importing?
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 790dfd4d480..40277a9b139 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -8,8 +8,8 @@ class LegacyDiffNote < Note
before_create :set_diff
class << self
- def build_discussion_id(noteable_type, noteable_id, line_code, active = true)
- [super(noteable_type, noteable_id), line_code, active].join("-")
+ def build_discussion_id(noteable_type, noteable_id, line_code)
+ [super(noteable_type, noteable_id), line_code].join("-")
end
end
@@ -21,8 +21,12 @@ class LegacyDiffNote < Note
{ line_code: line_code }
end
- def discussion_id
- @discussion_id ||= self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
+ def project_repository
+ if RequestStore.active?
+ RequestStore.fetch("project:#{project_id}:repository") { self.project.repository }
+ else
+ self.project.repository
+ end
end
def diff_file_hash
@@ -34,17 +38,21 @@ class LegacyDiffNote < Note
end
def diff_file
- @diff_file ||= Gitlab::Diff::File.new(diff, repository: self.project.repository) if diff
+ @diff_file ||= Gitlab::Diff::File.new(diff, repository: project_repository) if diff
end
def diff_line
- @diff_line ||= diff_file.line_for_line_code(self.line_code)
+ @diff_line ||= diff_file.line_for_line_code(self.line_code) if diff_file
end
def for_line?(line)
!line.meta? && diff_file.line_code(line) == self.line_code
end
+ def original_line_code
+ self.line_code
+ end
+
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
@@ -55,7 +63,7 @@ class LegacyDiffNote < Note
def active?
return @active if defined?(@active)
return true if for_commit?
- return true unless self.diff
+ return true unless diff_line
return false unless noteable
noteable_diff = find_noteable_diff
@@ -77,7 +85,7 @@ class LegacyDiffNote < Note
return nil unless noteable
return @diff if defined?(@diff)
- @diff = noteable.diffs(Commit.max_diff_options).find do |d|
+ @diff = noteable.raw_diffs(Commit.max_diff_options).find do |d|
d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash
end
end
@@ -108,7 +116,11 @@ class LegacyDiffNote < Note
# Find the diff on noteable that matches our own
def find_noteable_diff
- diffs = noteable.diffs(Commit.max_diff_options)
+ diffs = noteable.raw_diffs(Commit.max_diff_options)
diffs.find { |d| d.new_path == self.diff.new_path }
end
+
+ def build_discussion_id
+ self.class.build_discussion_id(noteable_type, noteable_id || commit_id, line_code)
+ end
end
diff --git a/app/models/list.rb b/app/models/list.rb
new file mode 100644
index 00000000000..eb87decdbc8
--- /dev/null
+++ b/app/models/list.rb
@@ -0,0 +1,34 @@
+class List < ActiveRecord::Base
+ belongs_to :board
+ belongs_to :label
+
+ enum list_type: { backlog: 0, label: 1, done: 2 }
+
+ validates :board, :list_type, presence: true
+ validates :label, :position, presence: true, if: :label?
+ validates :label_id, uniqueness: { scope: :board_id }, if: :label?
+ validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label?
+
+ before_destroy :can_be_destroyed
+
+ scope :destroyable, -> { where(list_type: list_types[:label]) }
+ scope :movable, -> { where(list_type: list_types[:label]) }
+
+ def destroyable?
+ label?
+ end
+
+ def movable?
+ label?
+ end
+
+ def title
+ label? ? label.name : list_type.humanize
+ end
+
+ private
+
+ def can_be_destroyed
+ destroyable?
+ end
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index 44db3d977fa..64e0d33fb20 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
class Member < ActiveRecord::Base
include Sortable
include Importable
+ include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -53,6 +54,10 @@ class Member < ActiveRecord::Base
default_value_for :notification_level, NotificationSetting.levels[:global]
class << self
+ def access_for_user_ids(user_ids)
+ where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
+ end
+
def find_by_invite_token(invite_token)
invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
find_by(invite_token: invite_token)
@@ -69,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
- def add_user(members, user_id, access_level, current_user = nil)
+ def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
@@ -83,6 +88,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
+ member.expires_at = expires_at
member.save
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index f39afc61ce9..ec2d40eb11c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -8,6 +8,7 @@ class ProjectMember < Member
# Make sure project member points only to project as it source
default_value_for :source_type, SOURCE_TYPE
validates_format_of :source_type, with: /\AProject\z/
+ validates :access_level, inclusion: { in: Gitlab::Access.values }
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
@@ -21,19 +22,19 @@ class ProjectMember < Member
# or symbol like :master representing role
#
# Ex.
- # add_users_into_projects(
+ # add_users_to_projects(
# project_ids,
# user_ids,
# ProjectMember::MASTER
# )
#
- # add_users_into_projects(
+ # add_users_to_projects(
# project_ids,
# user_ids,
# :master
# )
#
- def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -49,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
- Member.add_user(project.project_members, user, access_level, current_user)
+ Member.add_user(
+ project.project_members,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 393d8a72657..1d05e4a85d1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -10,16 +10,18 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
belongs_to :merge_user, class_name: "User"
- has_one :merge_request_diff, dependent: :destroy
+ has_many :merge_request_diffs, dependent: :destroy
+ has_one :merge_request_diff,
+ -> { order('merge_request_diffs.id DESC') }
has_many :events, as: :target, dependent: :destroy
serialize :merge_params, Hash
- after_create :create_merge_request_diff, unless: :importing?
- after_update :update_merge_request_diff
+ after_create :ensure_merge_request_diff, unless: :importing?
+ after_update :reload_diff_if_branch_changed
- delegate :commits, :diffs, :real_size, to: :merge_request_diff, prefix: nil
+ delegate :commits, :real_size, 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
@@ -104,6 +106,7 @@ class MergeRequest < ActiveRecord::Base
scope :from_project, ->(project) { where(source_project_id: project.id) }
scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
+ scope :from_source_branches, ->(branches) { where(source_branch: branches) }
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
@@ -164,6 +167,18 @@ class MergeRequest < ActiveRecord::Base
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
end
+ def raw_diffs(*args)
+ merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args)
+ end
+
+ def diffs(diff_options = nil)
+ if compare
+ compare.diffs(diff_options)
+ else
+ merge_request_diff.diffs(diff_options)
+ end
+ end
+
def diff_size
merge_request_diff.size
end
@@ -171,8 +186,8 @@ class MergeRequest < ActiveRecord::Base
def diff_base_commit
if persisted?
merge_request_diff.base_commit
- elsif diff_start_commit && diff_head_commit
- self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
+ else
+ branch_merge_base_commit
end
end
@@ -233,12 +248,21 @@ class MergeRequest < ActiveRecord::Base
target_project.repository.commit(target_branch) if target_branch_ref
end
+ def branch_merge_base_commit
+ start_sha = target_branch_sha
+ head_sha = source_branch_sha
+
+ if start_sha && head_sha
+ target_project.merge_base_commit(start_sha, head_sha)
+ end
+ end
+
def target_branch_sha
- target_branch_head.try(:sha)
+ @target_branch_sha || target_branch_head.try(:sha)
end
def source_branch_sha
- source_branch_head.try(:sha)
+ @source_branch_sha || source_branch_head.try(:sha)
end
def diff_refs
@@ -251,6 +275,19 @@ class MergeRequest < ActiveRecord::Base
)
end
+ # Return diff_refs instance trying to not touch the git repository
+ def diff_sha_refs
+ if merge_request_diff && merge_request_diff.diff_refs_by_sha?
+ merge_request_diff.diff_refs
+ else
+ diff_refs
+ end
+ end
+
+ def branch_merge_base_sha
+ branch_merge_base_commit.try(:sha)
+ end
+
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -283,19 +320,31 @@ class MergeRequest < ActiveRecord::Base
end
end
- def update_merge_request_diff
+ def ensure_merge_request_diff
+ merge_request_diff || create_merge_request_diff
+ end
+
+ def create_merge_request_diff
+ merge_request_diffs.create
+ reload_merge_request_diff
+ end
+
+ def reload_merge_request_diff
+ merge_request_diff(true)
+ end
+
+ def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed?
reload_diff
end
end
def reload_diff
- return unless merge_request_diff && open?
+ return unless open?
old_diff_refs = self.diff_refs
-
- merge_request_diff.reload_content
-
+ create_merge_request_diff
+ MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
update_diff_notes_positions(
@@ -390,6 +439,32 @@ class MergeRequest < ActiveRecord::Base
)
end
+ def discussions
+ @discussions ||= self.mr_and_commit_notes.
+ inc_relations_for_view.
+ fresh.
+ discussions
+ end
+
+ def diff_discussions
+ @diff_discussions ||= self.notes.diff_notes.discussions
+ end
+
+ def find_diff_discussion(discussion_id)
+ notes = self.notes.diff_notes.where(discussion_id: discussion_id).fresh.to_a
+ return if notes.empty?
+
+ Discussion.new(notes)
+ end
+
+ def discussions_resolvable?
+ diff_discussions.any?(&:resolvable?)
+ end
+
+ def discussions_resolved?
+ discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
+ end
+
def hook_attrs
attrs = {
source: source_project.try(:hook_attrs),
@@ -548,7 +623,13 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_merged_by?(user)
- ::Gitlab::GitAccess.new(user, project, 'web').can_push_to_branch?(target_branch)
+ access = ::Gitlab::UserAccess.new(user, project: project)
+ access.can_push_to_branch?(target_branch) || access.can_merge_to_branch?(target_branch)
+ end
+
+ def can_be_merged_via_command_line_by?(user)
+ access = ::Gitlab::UserAccess.new(user, project: project)
+ access.can_push_to_branch?(target_branch)
end
def mergeable_ci_state?
@@ -557,6 +638,14 @@ class MergeRequest < ActiveRecord::Base
!pipeline || pipeline.success?
end
+ def environments
+ return unless diff_head_commit
+
+ target_project.environments.select do |environment|
+ environment.includes_commit?(diff_head_commit)
+ end
+ end
+
def state_human_name
if merged?
"Merged"
@@ -632,10 +721,21 @@ class MergeRequest < ActiveRecord::Base
diverged_commits_count > 0
end
+ def commits_sha
+ commits.map(&:sha)
+ end
+
def pipeline
@pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
end
+ def all_pipelines
+ @all_pipelines ||=
+ if diff_head_sha && source_project
+ source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch)
+ end
+ end
+
def merge_commit
@merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha
end
@@ -648,12 +748,12 @@ class MergeRequest < ActiveRecord::Base
merge_commit
end
- def support_new_diff_notes?
- diff_refs && diff_refs.complete?
+ def has_complete_diff_refs?
+ diff_sha_refs && diff_sha_refs.complete?
end
def update_diff_notes_positions(old_diff_refs:, new_diff_refs:)
- return unless support_new_diff_notes?
+ return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
active_diff_notes = self.notes.diff_notes.select do |note|
@@ -681,4 +781,30 @@ class MergeRequest < ActiveRecord::Base
def keep_around_commit
project.repository.keep_around(self.merge_commit_sha)
end
+
+ def conflicts
+ @conflicts ||= Gitlab::Conflict::FileCollection.new(self)
+ end
+
+ def conflicts_can_be_resolved_by?(user)
+ access = ::Gitlab::UserAccess.new(user, project: source_project)
+ access.can_push_to_branch?(source_branch)
+ end
+
+ def conflicts_can_be_resolved_in_ui?
+ return @conflicts_can_be_resolved_in_ui if defined?(@conflicts_can_be_resolved_in_ui)
+
+ return @conflicts_can_be_resolved_in_ui = false unless cannot_be_merged?
+ return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
+
+ begin
+ # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
+ # ensure that we don't say there are conflicts to resolve when there are no conflict
+ # files.
+ conflicts.files.each(&:lines)
+ @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ @conflicts_can_be_resolved_in_ui = false
+ end
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index ba235750aeb..445179a4487 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,14 +1,13 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
+ include EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
belongs_to :merge_request
- delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -23,30 +22,63 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ # All diff information is collected from repository after object is created.
+ # It allows you to override variables like head_commit_sha before getting diff.
+ after_create :save_git_content, unless: :importing?
+
+ def self.select_without_diff
+ select(column_names - ['st_diffs'])
+ end
- def reload_content
+ # Collect information about commits and diff from repository
+ # and save it to the database as serialized data
+ def save_git_content
+ ensure_commits_sha
+ save_commits
reload_commits
- reload_diffs
+ save_diffs
+ keep_around_commits
+ end
+
+ def ensure_commits_sha
+ merge_request.fetch_ref
+ self.start_commit_sha ||= merge_request.target_branch_sha
+ self.head_commit_sha ||= merge_request.source_branch_sha
+ self.base_commit_sha ||= find_base_sha
+ save
+ end
+
+ # Override head_commit_sha to keep compatibility with merge request diff
+ # created before version 8.4 that does not store head_commit_sha in separate db field.
+ def head_commit_sha
+ if persisted? && super.nil?
+ last_commit.try(:sha)
+ else
+ super
+ end
+ end
+
+ # This method will rely on repository branch sha
+ # in case start_commit_sha is nil. Its necesarry for old merge request diff
+ # created before version 8.4 to work
+ def safe_start_commit_sha
+ start_commit_sha || merge_request.target_branch_sha
end
def size
- real_size.presence || diffs.size
+ real_size.presence || raw_diffs.size
end
- def diffs(options={})
+ def raw_diffs(options = {})
if options[:ignore_whitespace_change]
- @diffs_no_whitespace ||= begin
- compare = Gitlab::Git::Compare.new(
+ @diffs_no_whitespace ||=
+ Gitlab::Git::Compare.new(
repository.raw_repository,
- self.start_commit_sha || self.target_branch_sha,
- self.head_commit_sha || self.source_branch_sha,
- )
- compare.diffs(options)
- end
+ safe_start_commit_sha,
+ head_commit_sha).diffs(options)
else
- @diffs ||= load_diffs(st_diffs, options)
+ @raw_diffs ||= {}
+ @raw_diffs[options] ||= load_diffs(st_diffs, options)
end
end
@@ -54,6 +86,11 @@ class MergeRequestDiff < ActiveRecord::Base
@commits ||= load_commits(st_commits || [])
end
+ def reload_commits
+ @commits = nil
+ commits
+ end
+
def last_commit
commits.first
end
@@ -63,51 +100,60 @@ class MergeRequestDiff < ActiveRecord::Base
end
def base_commit
- return unless self.base_commit_sha
+ return unless base_commit_sha
- project.commit(self.base_commit_sha)
+ project.commit(base_commit_sha)
end
def start_commit
- return unless self.start_commit_sha
+ return unless start_commit_sha
- project.commit(self.start_commit_sha)
+ project.commit(start_commit_sha)
end
def head_commit
- return last_commit unless self.head_commit_sha
+ return unless head_commit_sha
- project.commit(self.head_commit_sha)
+ project.commit(head_commit_sha)
end
- def compare
- @compare ||=
- begin
- # Update ref for merge request
- merge_request.fetch_ref
+ def diff_refs
+ return unless start_commit_sha || base_commit_sha
- Gitlab::Git::Compare.new(
- repository.raw_repository,
- self.target_branch_sha,
- self.source_branch_sha
- )
- end
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: base_commit_sha,
+ start_sha: start_commit_sha,
+ head_sha: head_commit_sha
+ )
end
- private
+ def diff_refs_by_sha?
+ base_commit_sha? && head_commit_sha? && start_commit_sha?
+ end
- # Collect array of Git::Commit objects
- # between target and source branches
- def unmerged_commits
- commits = compare.commits
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
+ end
- if commits.present?
- commits = Commit.decorate(commits, merge_request.source_project).reverse
- end
+ def project
+ merge_request.target_project
+ end
- commits
+ def compare
+ @compare ||=
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ safe_start_commit_sha,
+ head_commit_sha
+ )
end
+ def latest?
+ self == merge_request.merge_request_diff
+ end
+
+ private
+
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -116,26 +162,21 @@ class MergeRequestDiff < ActiveRecord::Base
array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
- # Reload all commits related to current merge request from repo
+ # Load all commits related to current merge request diff from repo
# and save it as array of hashes in st_commits db field
- def reload_commits
+ def save_commits
new_attributes = {}
- commit_objects = unmerged_commits
+ commits = compare.commits
- if commit_objects.present?
- new_attributes[:st_commits] = dump_commits(commit_objects)
+ if commits.present?
+ commits = Commit.decorate(commits, merge_request.source_project).reverse
+ new_attributes[:st_commits] = dump_commits(commits)
end
update_columns_serialized(new_attributes)
end
- # Collect array of Git::Diff objects
- # between target and source branches
- def unmerged_diffs
- compare.diffs(Commit.max_diff_options)
- end
-
def dump_diffs(diffs)
if diffs.respond_to?(:map)
diffs.map(&:to_hash)
@@ -144,22 +185,28 @@ class MergeRequestDiff < ActiveRecord::Base
def load_diffs(raw, options)
if raw.respond_to?(:each)
+ if paths = options[:paths]
+ raw = raw.select do |diff|
+ paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
+ end
+ end
+
Gitlab::Git::DiffCollection.new(raw, options)
else
Gitlab::Git::DiffCollection.new([])
end
end
- # Reload diffs between branches related to current merge request from repo
+ # Load diffs between branches related to current merge request diff from repo
# and save it as array of hashes in st_diffs db field
- def reload_diffs
+ def save_diffs
new_attributes = {}
new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
- diff_collection = unmerged_diffs
+ diff_collection = compare.diffs(Commit.max_diff_options)
if diff_collection.overflow?
# Set our state to 'overflow' to make the #empty? and #collected?
@@ -176,32 +223,25 @@ class MergeRequestDiff < ActiveRecord::Base
end
new_attributes[:st_diffs] = new_diffs
-
- new_attributes[:start_commit_sha] = self.target_branch_sha
- new_attributes[:head_commit_sha] = self.source_branch_sha
- new_attributes[:base_commit_sha] = branch_base_sha
-
update_columns_serialized(new_attributes)
-
- keep_around_commits
- end
-
- def project
- merge_request.target_project
end
def repository
project.repository
end
- def branch_base_commit
- return unless self.source_branch_sha && self.target_branch_sha
+ def find_base_sha
+ return unless head_commit_sha && start_commit_sha
- project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
+ project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end
- def branch_base_sha
- branch_base_commit.try(:sha)
+ def utf8_st_diffs
+ st_diffs.map do |diff|
+ diff.each do |k, v|
+ diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
+ end
+ end
end
#
@@ -228,8 +268,8 @@ class MergeRequestDiff < ActiveRecord::Base
end
def keep_around_commits
- repository.keep_around(target_branch_sha)
- repository.keep_around(source_branch_sha)
- repository.keep_around(branch_base_sha)
+ repository.keep_around(start_commit_sha)
+ repository.keep_around(head_commit_sha)
+ repository.keep_around(base_commit_sha)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 8b52cc824cd..7c29d27ce97 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,4 +1,6 @@
class Namespace < ActiveRecord::Base
+ acts_as_paranoid
+
include Sortable
include Gitlab::ShellAdapter
diff --git a/app/models/note.rb b/app/models/note.rb
index ffffd0c0838..f2656df028b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -5,11 +5,16 @@ class Note < ActiveRecord::Base
include Mentionable
include Awardable
include Importable
+ include FasterCacheKeys
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
attr_accessor :note_html
+ # An Array containing the number of visible references as generated by
+ # Banzai::ObjectRenderer
+ attr_accessor :user_visible_reference_count
+
default_value_for :system, false
attr_mentionable :note, pipeline: :note
@@ -20,6 +25,9 @@ class Note < ActiveRecord::Base
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
+ # Only used by DiffNote, but defined here so that it can be used in `Note.includes`
+ belongs_to :resolved_by, class_name: "User"
+
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
@@ -54,7 +62,7 @@ class Note < ActiveRecord::Base
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
- scope :inc_author_project_award_emoji, ->{ includes(:project, :author, :award_emoji) }
+ scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
scope :diff_notes, ->{ where(type: ['LegacyDiffNote', 'DiffNote']) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
@@ -65,7 +73,9 @@ class Note < ActiveRecord::Base
project: [:project_members, { group: [:group_members] }])
end
- before_validation :clear_blank_line_code!
+ after_initialize :ensure_discussion_id
+ before_validation :nullify_blank_type, :nullify_blank_line_code
+ before_validation :set_discussion_id
after_save :keep_around_commit
class << self
@@ -77,12 +87,18 @@ class Note < ActiveRecord::Base
[:discussion, noteable_type.try(:underscore), noteable_id].join("-")
end
+ def discussion_id(*args)
+ Digest::SHA1.hexdigest(build_discussion_id(*args))
+ end
+
def discussions
- all.group_by(&:discussion_id).values
+ Discussion.for_notes(all)
end
- def grouped_diff_notes
- diff_notes.select(&:active?).sort_by(&:created_at).group_by(&:line_code)
+ def grouped_diff_discussions
+ active_notes = diff_notes.fresh.select(&:active?)
+ Discussion.for_diff_notes(active_notes).
+ map { |d| [d.line_code, d] }.to_h
end
# Searches for notes matching the given query.
@@ -123,13 +139,16 @@ class Note < ActiveRecord::Base
true
end
- def discussion_id
- @discussion_id ||=
- if for_merge_request?
- [:discussion, :note, id].join("-")
- else
- self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
- end
+ def resolvable?
+ false
+ end
+
+ def resolved?
+ false
+ end
+
+ def to_be_resolved?
+ resolvable? && !resolved?
end
def max_attachment_size
@@ -193,7 +212,15 @@ class Note < ActiveRecord::Base
end
def cross_reference_not_visible_for?(user)
- cross_reference? && referenced_mentionables(user).empty?
+ cross_reference? && !has_referenced_mentionables?(user)
+ end
+
+ def has_referenced_mentionables?(user)
+ if user_visible_reference_count.present?
+ user_visible_reference_count > 0
+ else
+ referenced_mentionables(user).any?
+ end
end
def award_emoji?
@@ -204,10 +231,6 @@ class Note < ActiveRecord::Base
!system?
end
- def clear_blank_line_code!
- self.line_code = nil if self.line_code.blank?
- end
-
def can_be_award_emoji?
noteable.is_a?(Awardable)
end
@@ -217,8 +240,7 @@ class Note < ActiveRecord::Base
end
def award_emoji_name
- original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
- Gitlab::AwardEmoji.normalize_emoji_name(original_name)
+ note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
end
private
@@ -226,4 +248,36 @@ class Note < ActiveRecord::Base
def keep_around_commit
project.repository.keep_around(self.commit_id)
end
+
+ def nullify_blank_type
+ self.type = nil if self.type.blank?
+ end
+
+ def nullify_blank_line_code
+ self.line_code = nil if self.line_code.blank?
+ end
+
+ def ensure_discussion_id
+ return unless self.persisted?
+ # Needed in case the SELECT statement doesn't ask for `discussion_id`
+ return unless self.has_attribute?(:discussion_id)
+ return if self.discussion_id
+
+ set_discussion_id
+ update_column(:discussion_id, self.discussion_id)
+ end
+
+ def set_discussion_id
+ self.discussion_id = Digest::SHA1.hexdigest(build_discussion_id)
+ end
+
+ def build_discussion_id
+ if for_merge_request?
+ # Notes on merge requests are always in a discussion of their own,
+ # so we generate a unique discussion ID.
+ [:discussion, :note, SecureRandom.hex].join("-")
+ else
+ self.class.build_discussion_id(noteable_type, noteable_id || commit_id)
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index a66b750cd48..0e4fb94f8eb 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -62,6 +62,8 @@ class Project < ActiveRecord::Base
belongs_to :group, -> { where(type: Group) }, foreign_key: 'namespace_id'
belongs_to :namespace
+ has_one :board, dependent: :destroy
+
has_one :last_event, -> {order 'events.created_at DESC'}, class_name: 'Event', foreign_key: 'project_id'
# Project services
@@ -197,6 +199,8 @@ class Project < ActiveRecord::Base
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) }
+ scope :excluding_project, ->(project) { where.not(id: project) }
+
state_machine :import_status, initial: :none do
event :import_start do
transition [:none, :finished] => :started
@@ -379,9 +383,10 @@ class Project < ActiveRecord::Base
joins(join_body).reorder('join_note_counts.amount DESC')
end
- # Deletes gitlab project export files older than 24 hours
- def remove_gitlab_exports!
- Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete))
+ def cached_count
+ Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do
+ Project.count
+ end
end
end
@@ -429,6 +434,17 @@ class Project < ActiveRecord::Base
repository.commit(ref)
end
+ # ref can't be HEAD, can only be branch/tag name or SHA
+ def latest_successful_builds_for(ref = default_branch)
+ latest_pipeline = pipelines.latest_successful_for(ref).first
+
+ if latest_pipeline
+ latest_pipeline.builds.latest.with_artifacts
+ else
+ builds.none
+ end
+ end
+
def merge_base_commit(first_commit_id, second_commit_id)
sha = repository.merge_base(first_commit_id, second_commit_id)
repository.commit(sha) if sha
@@ -440,7 +456,9 @@ class Project < ActiveRecord::Base
def add_import_job
if forked?
- job_id = RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
+ job_id = RepositoryForkWorker.perform_async(id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace,
+ self.namespace.path)
else
job_id = RepositoryImportWorker.perform_async(self.id)
end
@@ -453,8 +471,6 @@ class Project < ActiveRecord::Base
end
def reset_cache_and_import_attrs
- update(import_error: nil)
-
ProjectCacheWorker.perform_async(self.id)
self.import_data.destroy if self.import_data
@@ -464,8 +480,8 @@ class Project < ActiveRecord::Base
return super(value) unless Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
- create_or_update_import_data(credentials: import_url.credentials)
super(import_url.sanitized_url)
+ create_or_update_import_data(credentials: import_url.credentials)
end
def import_url
@@ -477,7 +493,13 @@ class Project < ActiveRecord::Base
end
end
+ def valid_import_url?
+ valid? || errors.messages[:import_url].nil?
+ end
+
def create_or_update_import_data(data: nil, credentials: nil)
+ return unless import_url.present? && valid_import_url?
+
project_import_data = import_data || build_import_data
if data
project_import_data.data ||= {}
@@ -567,7 +589,11 @@ class Project < ActiveRecord::Base
end
def to_param
- path
+ if persisted? && errors.include?(:path)
+ path_was
+ else
+ path
+ end
end
def to_reference(_from_project = nil)
@@ -582,6 +608,16 @@ class Project < ActiveRecord::Base
web_url.split('://')[1]
end
+ def new_issue_address(author)
+ # This feature is disabled for the time being.
+ return nil
+
+ if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
+ Gitlab::IncomingEmail.reply_address(
+ "#{path_with_namespace}+#{author.authentication_token}")
+ end
+ end
+
def build_commit_note(commit)
notes.new(commit_id: commit.id, noteable_type: 'Commit')
end
@@ -644,6 +680,22 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
+ def external_wiki
+ if has_external_wiki.nil?
+ cache_has_external_wiki # Populate
+ end
+
+ if has_external_wiki
+ @external_wiki ||= services.external_wikis.first
+ else
+ nil
+ end
+ end
+
+ def cache_has_external_wiki
+ update_column(:has_external_wiki, services.external_wikis.any?)
+ end
+
def build_missing_services
services_templates = Service.where(template: true)
@@ -824,12 +876,14 @@ class Project < ActiveRecord::Base
# Check if current branch name is marked as protected in the system
def protected_branch?(branch_name)
+ return true if empty_repo? && default_branch_protected?
+
@protected_branches ||= self.protected_branches.to_a
ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
end
- def developers_can_push_to_protected_branch?(branch_name)
- protected_branches.matching(branch_name).any?(&:developers_can_push)
+ def user_can_push_to_empty_repo?(user)
+ !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
def forked?
@@ -845,9 +899,13 @@ class Project < ActiveRecord::Base
old_path_with_namespace = File.join(namespace_dir, path_was)
new_path_with_namespace = File.join(namespace_dir, path)
+ Rails.logger.error "Attempting to rename #{old_path_with_namespace} -> #{new_path_with_namespace}"
+
expire_caches_before_rename(old_path_with_namespace)
if has_container_registry_tags?
+ Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present"
+
# we currently doesn't support renaming repository if it contains tags in container registry
raise Exception.new('Project cannot be renamed, because tags are present in its container registry')
end
@@ -866,17 +924,22 @@ class Project < ActiveRecord::Base
SystemHooksService.new.execute_hooks_for(self, :rename)
@repository = nil
- rescue
+ rescue => e
+ Rails.logger.error "Exception renaming #{old_path_with_namespace} -> #{new_path_with_namespace}: #{e}"
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
false
end
else
+ Rails.logger.error "Repository could not be renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
+
# 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('repository cannot be renamed')
end
+ Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}"
+
Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path)
end
@@ -941,6 +1004,10 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
+ end
+
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
@@ -968,6 +1035,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
+ repository.expire_avatar_cache(branch)
reload_default_branch
end
@@ -1032,8 +1100,8 @@ class Project < ActiveRecord::Base
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_pipeline(sha, ref)
- pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref)
+ def ensure_pipeline(sha, ref, current_user = nil)
+ pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
end
def enable_ci
@@ -1103,13 +1171,6 @@ class Project < ActiveRecord::Base
@wiki ||= ProjectWiki.new(self, self.owner)
end
- def schedule_delete!(user_id, params)
- # Queue this task for after the commit, so once we mark pending_delete it will run
- run_after_commit { ProjectDestroyWorker.perform_async(id, user_id, params) }
-
- update_attribute(:pending_delete, true)
- end
-
def running_or_pending_build_count(force: false)
Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do
builds.running_or_pending.count(:all)
@@ -1154,4 +1215,89 @@ class Project < ActiveRecord::Base
def ensure_dir_exist
gitlab_shell.add_namespace(repository_storage_path, namespace.path)
end
+
+ def predefined_variables
+ [
+ { key: 'CI_PROJECT_ID', value: id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: path, public: true },
+ { key: 'CI_PROJECT_PATH', value: path_with_namespace, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: namespace.path, public: true },
+ { key: 'CI_PROJECT_URL', value: web_url, public: true }
+ ]
+ end
+
+ def container_registry_variables
+ return [] unless Gitlab.config.registry.enabled
+
+ variables = [
+ { key: 'CI_REGISTRY', value: Gitlab.config.registry.host_port, public: true }
+ ]
+
+ if container_registry_enabled?
+ variables << { key: 'CI_REGISTRY_IMAGE', value: container_registry_repository_url, public: true }
+ end
+
+ variables
+ end
+
+ def secret_variables
+ variables.map do |variable|
+ { key: variable.key, value: variable.value, public: false }
+ 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
+
+ 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)
+ end
+
+ def append_or_update_attribute(name, value)
+ old_values = public_send(name.to_s)
+
+ if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any?
+ update_attribute(name, old_values + value)
+ else
+ update_attribute(name, value)
+ end
+ end
+
+ private
+
+ 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
end
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c84..7613cbdea93 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
class ProjectGroupLink < ActiveRecord::Base
+ include Expirable
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
- private
+ private
def different_group
if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 54da4d74fc5..fa66e5864b8 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -42,6 +42,18 @@ class BuildsEmailService < Service
end
end
+ def can_test?
+ project.builds.count > 0
+ end
+
+ def disabled_title
+ "Please setup a build on your repository."
+ end
+
+ def test_data(project = nil, user = nil)
+ Gitlab::DataBuilder::Build.build(project.builds.last)
+ end
+
def fields
[
{ type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
@@ -50,6 +62,20 @@ class BuildsEmailService < Service
]
end
+ def test(data)
+ begin
+ # bypass build status verification when testing
+ data[:build_status] = "failed"
+ data[:build_allow_failure] = false
+
+ result = execute(data)
+ rescue StandardError => error
+ return { success: false, result: error }
+ end
+
+ { success: true, result: result }
+ end
+
def should_build_be_notified?(data)
case data[:build_status]
when 'success'
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 511b2eac792..5af93860d09 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -1,4 +1,6 @@
class CampfireService < Service
+ include HTTParty
+
prop_accessor :token, :subdomain, :room
validates :token, presence: true, if: :activated?
@@ -29,18 +31,53 @@ class CampfireService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
- room = gate.find_room_by_name(self.room)
- return true unless room
-
+ self.class.base_uri base_uri
message = build_message(data)
-
- room.speak(message)
+ speak(self.room, message, auth)
end
private
- def gate
- @gate ||= Tinder::Campfire.new(subdomain, token: token)
+ def base_uri
+ @base_uri ||= "https://#{subdomain}.campfirenow.com"
+ end
+
+ def auth
+ # use a dummy password, as explained in the Campfire API doc:
+ # https://github.com/basecamp/campfire-api#authentication
+ @auth ||= {
+ basic_auth: {
+ username: token,
+ password: 'X'
+ }
+ }
+ end
+
+ # Post a message into a room, returns the message Hash in case of success.
+ # Returns nil otherwise.
+ # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message
+ def speak(room_name, message, auth)
+ room = rooms(auth).find { |r| r["name"] == room_name }
+ return nil unless room
+
+ path = "/room/#{room["id"]}/speak.json"
+ body = {
+ body: {
+ message: {
+ type: 'TextMessage',
+ body: message
+ }
+ }
+ }
+ res = self.class.post(path, auth.merge(body))
+ res.code == 201 ? res : nil
+ end
+
+ # Returns a list of rooms, or [].
+ # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
+ def rooms(auth)
+ res = self.class.get("/rooms.json", auth)
+ res.code == 200 ? res["rooms"] : []
end
def build_message(push)
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 23e5b16221b..d7c986c1a91 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -46,7 +46,7 @@ class HipchatService < Service
return unless supported_events.include?(data[:object_kind])
message = create_message(data)
return unless message.present?
- gate[room].send('GitLab', message, message_options)
+ gate[room].send('GitLab', message, message_options(data))
end
def test(data)
@@ -67,8 +67,8 @@ class HipchatService < Service
@gate ||= HipChat::Client.new(token, options)
end
- def message_options
- { notify: notify.present? && notify == '1', color: color || 'yellow' }
+ def message_options(data = nil)
+ { notify: notify.present? && notify == '1', color: message_color(data) }
end
def create_message(data)
@@ -240,6 +240,21 @@ class HipchatService < Service
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
+ def message_color(data)
+ build_status_color(data) || color || 'yellow'
+ end
+
+ def build_status_color(data)
+ return unless data && data[:object_kind] == 'build'
+
+ case data[:commit][:status]
+ when 'success'
+ 'green'
+ else
+ 'red'
+ end
+ end
+
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ad19b7795da..5301f9fa0ff 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -1,7 +1,9 @@
class PivotaltrackerService < Service
include HTTParty
- prop_accessor :token
+ API_ENDPOINT = 'https://www.pivotaltracker.com/services/v5/source_commits'
+
+ prop_accessor :token, :restrict_to_branch
validates :token, presence: true, if: :activated?
def title
@@ -18,7 +20,17 @@ class PivotaltrackerService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' }
+ {
+ type: 'text',
+ name: 'token',
+ placeholder: 'Pivotal Tracker API token.'
+ },
+ {
+ type: 'text',
+ name: 'restrict_to_branch',
+ placeholder: 'Comma-separated list of branches which will be ' \
+ 'automatically inspected. Leave blank to include all branches.'
+ }
]
end
@@ -28,8 +40,8 @@ class PivotaltrackerService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
+ return unless allowed_branch?(data[:ref])
- url = 'https://www.pivotaltracker.com/services/v5/source_commits'
data[:commits].each do |commit|
message = {
'source_commit' => {
@@ -40,7 +52,7 @@ class PivotaltrackerService < Service
}
}
PivotaltrackerService.post(
- url,
+ API_ENDPOINT,
body: message.to_json,
headers: {
'Content-Type' => 'application/json',
@@ -49,4 +61,15 @@ class PivotaltrackerService < Service
)
end
end
+
+ private
+
+ def allowed_branch?(ref)
+ return true unless ref.present? && restrict_to_branch.present?
+
+ branch = Gitlab::Git.ref_name(ref)
+ allowed_branches = restrict_to_branch.split(',').map(&:strip)
+
+ branch.present? && allowed_branches.include?(branch)
+ end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index cf9e4d5a8b6..abbc780dc1a 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -4,6 +4,9 @@ class SlackService < Service
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
@@ -29,13 +32,15 @@ class SlackService < Service
end
def fields
- [
- { type: 'text', name: 'webhook',
- placeholder: 'https://hooks.slack.com/services/...' },
- { type: 'text', name: 'username', placeholder: 'username' },
- { type: 'text', name: 'channel', placeholder: '#channel' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- ]
+ 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' },
+ ]
+
+ default_fields + build_event_channels
end
def supported_events
@@ -74,7 +79,10 @@ class SlackService < Service
end
opt = {}
- opt[:channel] = channel if channel
+
+ event_channel = get_channel_field(object_kind) || channel
+
+ opt[:channel] = event_channel if event_channel
opt[:username] = username if username
if message
@@ -83,8 +91,35 @@ class SlackService < Service
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_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
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 0b700930641..ab6ea2aae36 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, access, current_user)
+ add_users(users, access, current_user: current_user)
else
- add_user(users, access, current_user)
+ add_user(users, access, current_user: current_user)
end
end
@@ -33,17 +33,18 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user = nil)
- ProjectMember.add_users_into_projects(
+ def add_users(users, access, current_user: nil, expires_at: nil)
+ ProjectMember.add_users_to_projects(
[project.id],
users,
access,
- current_user
+ current_user: current_user,
+ expires_at: expires_at
)
end
- def add_user(user, access, current_user = nil)
- add_users([user], access, current_user)
+ def add_user(user, access, current_user: nil, expires_at: nil)
+ add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
@@ -132,39 +133,68 @@ class ProjectTeam
Gitlab::Access.options_with_owner.key(max_member_access(user_id))
end
- # This method assumes project and group members are eager loaded for optimal
- # performance.
- def max_member_access(user_id)
- access = []
+ # Determine the maximum access level for a group of users in bulk.
+ #
+ # Returns a Hash mapping user ID -> maximum access level.
+ def max_member_access_for_user_ids(user_ids)
+ user_ids = user_ids.uniq
+ key = "max_member_access:#{project.id}"
- access += project.members.where(user_id: user_id).has_access.pluck(:access_level)
+ access = {}
- if group
- access += group.members.where(user_id: user_id).has_access.pluck(:access_level)
+ if RequestStore.active?
+ RequestStore.store[key] ||= {}
+ access = RequestStore.store[key]
end
- if project.invited_groups.any? && project.allowed_to_share_with_group?
- access << max_invited_level(user_id)
+ # Lookup only the IDs we need
+ user_ids = user_ids - access.keys
+
+ 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.invited_groups.any? && project.allowed_to_share_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.compact.max
+ access
+ end
+
+ def max_member_access(user_id)
+ max_member_access_for_user_ids([user_id])[user_id]
end
private
- def max_invited_level(user_id)
- project.project_group_links.map do |group_link|
- invited_group = group_link.group
- access = invited_group.group_members.find_by(user_id: user_id).try(:access_field)
+ # 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 group member has higher access level we should restrict it
- # to max allowed access level
- if access && access > group_link.group_access
- access = group_link.group_access
- end
+ # 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 }
- access
- end.compact.max
+ # 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)
@@ -173,7 +203,7 @@ class ProjectTeam
invited_members = []
if project.invited_groups.any? && project.allowed_to_share_with_group?
- project.project_group_links.each do |group_link|
+ project.project_group_links.includes(group: [:group_members]).each do |group_link|
invited_group = group_link.group
im = invited_group.members
@@ -215,4 +245,8 @@ class ProjectTeam
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
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a255710f577..46f70da2452 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -56,6 +56,10 @@ class ProjectWiki
end
end
+ def repository_exists?
+ !!repository.exists?
+ end
+
def empty?
pages.empty?
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index b7011d7afdf..6240912a6e1 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -5,6 +5,15 @@ class ProtectedBranch < ActiveRecord::Base
validates :name, presence: true
validates :project, presence: true
+ has_many :merge_access_levels, dependent: :destroy
+ has_many :push_access_levels, dependent: :destroy
+
+ validates_length_of :merge_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+ validates_length_of :push_access_levels, is: 1, message: "are restricted to a single instance per protected branch."
+
+ accepts_nested_attributes_for :push_access_levels
+ accepts_nested_attributes_for :merge_access_levels
+
def commit
project.commit(self.name)
end
diff --git a/app/models/protected_branch/merge_access_level.rb b/app/models/protected_branch/merge_access_level.rb
new file mode 100644
index 00000000000..806b3ccd275
--- /dev/null
+++ b/app/models/protected_branch/merge_access_level.rb
@@ -0,0 +1,22 @@
+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] }
+
+ def self.human_access_levels
+ {
+ Gitlab::Access::MASTER => "Masters",
+ 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
new file mode 100644
index 00000000000..92e9c51d883
--- /dev/null
+++ b/app/models/protected_branch/push_access_level.rb
@@ -0,0 +1,25 @@
+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] }
+
+ def self.human_access_levels
+ {
+ Gitlab::Access::MASTER => "Masters",
+ Gitlab::Access::DEVELOPER => "Developers + Masters",
+ Gitlab::Access::NO_ACCESS => "No one"
+ }.with_indifferent_access
+ end
+
+ 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
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5b670cb4b8f..91bdafdac99 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -11,16 +11,6 @@ class Repository
attr_accessor :path_with_namespace, :project
- def self.clean_old_archives
- Gitlab::Metrics.measure(:clean_old_archives) do
- repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
-
- return unless File.directory?(repository_downloads_path)
-
- Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
- end
- end
-
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -80,7 +70,12 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
- commit = Gitlab::Git::Commit.find(raw_repository, ref)
+ 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
@@ -168,7 +163,7 @@ class Repository
before_remove_branch
branch = find_branch(branch_name)
- oldrev = branch.try(:target)
+ oldrev = branch.try(:target).try(:id)
newrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
@@ -216,11 +211,23 @@ class Repository
return if kept_around?(sha)
- rugged.references.create(keep_around_ref_name(sha), sha)
+ # This will still fail if the file is corrupted (e.g. 0 bytes)
+ begin
+ rugged.references.create(keep_around_ref_name(sha), sha, force: true)
+ rescue Rugged::ReferenceError => ex
+ Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ rescue Rugged::OSError => ex
+ raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+ Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ end
end
def kept_around?(sha)
- ref_exists?(keep_around_ref_name(sha))
+ begin
+ ref_exists?(keep_around_ref_name(sha))
+ rescue Rugged::ReferenceError
+ false
+ end
end
def tag_names
@@ -257,10 +264,10 @@ class Repository
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind = raw_repository.
- count_commits_between(branch.target, root_ref_hash)
+ count_commits_between(branch.target.sha, root_ref_hash)
number_commits_ahead = raw_repository.
- count_commits_between(root_ref_hash, branch.target)
+ count_commits_between(root_ref_hash, branch.target.sha)
{ behind: number_commits_behind, ahead: number_commits_ahead }
end
@@ -270,7 +277,7 @@ class Repository
def cache_keys
%i(size commit_count
readme version contribution_guide changelog
- license_blob license_key gitignore)
+ license_blob license_key gitignore koding_yml)
end
# Keys for data on branch/tag operations.
@@ -365,7 +372,7 @@ class Repository
# 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.diffs.
+ return unless commit.raw_diffs(deltas_only: true).
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
end
@@ -384,6 +391,8 @@ class Repository
expire_exists_cache
expire_root_ref_cache
expire_emptiness_caches
+
+ repository_event(:create_repository)
end
# Runs code just before a repository is deleted.
@@ -392,9 +401,16 @@ class Repository
expire_cache if exists?
+ # expire cache that don't depend on repository data (when expiring)
+ expire_tags_cache
+ expire_tag_count_cache
+ expire_branches_cache
+ expire_branch_count_cache
expire_root_ref_cache
expire_emptiness_caches
expire_exists_cache
+
+ repository_event(:remove_repository)
end
# Runs code just before the HEAD of a repository is changed.
@@ -402,6 +418,8 @@ class Repository
# Cached divergent commit counts are based on repository head
expire_branch_cache
expire_root_ref_cache
+
+ repository_event(:change_default_branch)
end
# Runs code before pushing (= creating or removing) a tag.
@@ -409,12 +427,16 @@ class Repository
expire_cache
expire_tags_cache
expire_tag_count_cache
+
+ repository_event(:push_tag)
end
# Runs code before removing a tag.
def before_remove_tag
expire_tags_cache
expire_tag_count_cache
+
+ repository_event(:remove_tag)
end
def before_import
@@ -431,6 +453,8 @@ class Repository
# Runs code after a new commit has been pushed.
def after_push_commit(branch_name, revision)
expire_cache(branch_name, revision)
+
+ repository_event(:push_commit, branch: branch_name)
end
# Runs code after a new branch has been created.
@@ -438,11 +462,15 @@ class Repository
expire_branches_cache
expire_has_visible_content_cache
expire_branch_count_cache
+
+ repository_event(:push_branch)
end
# Runs code before removing an existing branch.
def before_remove_branch
expire_branches_cache
+
+ repository_event(:remove_branch)
end
# Runs code after an existing branch has been removed.
@@ -525,6 +553,14 @@ class Repository
end
end
+ def koding_yml
+ return nil unless head_exists?
+
+ cache.fetch(:koding_yml) do
+ file_on_head(/\A\.koding\.yml\z/)
+ end
+ end
+
def gitlab_ci_yml
return nil unless head_exists?
@@ -589,7 +625,7 @@ class Repository
commit(sha)
end
- def next_branch(name, opts={})
+ def next_branch(name, opts = {})
branch_ids = self.branch_names.map do |n|
next 1 if n == name
result = n.match(/\A#{name}-([0-9]+)\z/)
@@ -606,11 +642,13 @@ class Repository
# Remove archives older than 2 hours
def branches_sorted_by(value)
case value
- when 'recently_updated'
+ when 'name'
+ branches.sort_by(&:name)
+ when 'updated_desc'
branches.sort do |a, b|
commit(b.target).committed_date <=> commit(a.target).committed_date
end
- when 'last_updated'
+ when 'updated_asc'
branches.sort do |a, b|
commit(a.target).committed_date <=> commit(b.target).committed_date
end
@@ -622,9 +660,7 @@ class Repository
def tags_sorted_by(value)
case value
when 'name'
- # Would be better to use `sort_by` but `version_sorter` only exposes
- # `sort` and `rsort`
- VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) }
+ VersionSorter.rsort(tags) { |tag| tag.name }
when 'updated_desc'
tags_sorted_by_committed_date.reverse
when 'updated_asc'
@@ -679,9 +715,7 @@ class Repository
end
def local_branches
- @local_branches ||= rugged.branches.each(:local).map do |branch|
- Gitlab::Git::Branch.new(branch.name, branch.target)
- end
+ @local_branches ||= raw_repository.local_branches
end
alias_method :branches, :local_branches
@@ -704,6 +738,7 @@ class Repository
options[:commit] = {
message: message,
branch: ref,
+ update_ref: false,
}
raw_repository.mkdir(path, options)
@@ -719,6 +754,7 @@ class Repository
options[:commit] = {
message: message,
branch: ref,
+ update_ref: false,
}
options[:file] = {
@@ -731,6 +767,33 @@ class Repository
end
end
+ def update_file(user, path, content, branch:, previous_path:, message:)
+ commit_with_hooks(user, branch) do |ref|
+ committer = user_to_committer(user)
+ options = {}
+ options[:committer] = committer
+ options[:author] = committer
+ options[:commit] = {
+ message: message,
+ branch: ref,
+ update_ref: false
+ }
+
+ options[:file] = {
+ content: content,
+ path: path,
+ update: true
+ }
+
+ if previous_path
+ options[:file][:previous_path] = previous_path
+ Gitlab::Git::Blob.rename(raw_repository, options)
+ else
+ Gitlab::Git::Blob.commit(raw_repository, options)
+ end
+ end
+ end
+
def remove_file(user, path, message, branch)
commit_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
@@ -739,7 +802,8 @@ class Repository
options[:author] = committer
options[:commit] = {
message: message,
- branch: ref
+ branch: ref,
+ update_ref: false,
}
options[:file] = {
@@ -769,9 +833,9 @@ class Repository
end
end
- def merge(user, source_sha, target_branch, options = {})
- our_commit = rugged.branches[target_branch].target
- their_commit = rugged.lookup(source_sha)
+ def merge(user, merge_request, options = {})
+ our_commit = rugged.branches[merge_request.target_branch].target
+ their_commit = rugged.lookup(merge_request.diff_head_sha)
raise "Invalid merge target" if our_commit.nil?
raise "Invalid merge source" if their_commit.nil?
@@ -779,42 +843,42 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
- commit_with_hooks(user, target_branch) do |ref|
+ commit_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
- update_ref: ref
)
- Rugged::Commit.create(rugged, actual_options)
+ commit_id = Rugged::Commit.create(rugged, actual_options)
+ merge_request.update(in_progress_merge_commit_sha: commit_id)
+ commit_id
end
end
def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
revert_tree_id ||= check_revert_content(commit, base_branch)
return false unless revert_tree_id
- commit_with_hooks(user, base_branch) do |ref|
+ commit_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
author: committer,
committer: committer,
tree: revert_tree_id,
- parents: [rugged.lookup(source_sha)],
- update_ref: ref)
+ parents: [rugged.lookup(source_sha)])
end
end
def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
return false unless cherry_pick_tree_id
- commit_with_hooks(user, base_branch) do |ref|
+ commit_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.message,
@@ -825,13 +889,20 @@ class Repository
},
committer: committer,
tree: cherry_pick_tree_id,
- parents: [rugged.lookup(source_sha)],
- update_ref: ref)
+ parents: [rugged.lookup(source_sha)])
+ end
+ end
+
+ def resolve_conflicts(user, branch, params)
+ commit_with_hooks(user, branch) do
+ committer = user_to_committer(user)
+
+ Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
end
end
def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
@@ -845,7 +916,7 @@ class Repository
end
def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).target
+ source_sha = find_branch(base_branch).target.sha
args = [commit.id, source_sha]
args << 1 if commit.merge_commit?
@@ -927,20 +998,6 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
- def with_tmp_ref(oldrev = nil)
- random_string = SecureRandom.hex
- tmp_ref = "refs/tmp/#{random_string}/head"
-
- if oldrev && !Gitlab::Git.blank_ref?(oldrev)
- rugged.references.create(tmp_ref, oldrev)
- end
-
- # Make commit in tmp ref
- yield(tmp_ref)
- ensure
- rugged.references.delete(tmp_ref) rescue nil
- end
-
def commit_with_hooks(current_user, branch)
update_autocrlf_option
@@ -950,36 +1007,38 @@ class Repository
was_empty = empty?
if !was_empty && target_branch
- oldrev = target_branch.target
+ oldrev = target_branch.target.id
end
- with_tmp_ref(oldrev) do |tmp_ref|
- # Make commit in tmp ref
- newrev = yield(tmp_ref)
+ # Make commit
+ newrev = yield(ref)
- unless newrev
- raise CommitError.new('Failed to create commit')
- end
+ unless newrev
+ raise CommitError.new('Failed to create commit')
+ end
+
+ GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
+ if was_empty || !target_branch
+ # Create branch
+ rugged.references.create(ref, newrev)
+
+ # If repo was empty expire cache
+ after_create if was_empty
+ after_create_branch
+ else
+ # Update head
+ current_head = find_branch(branch).target.id
- GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
- if was_empty || !target_branch
- # Create branch
- rugged.references.create(ref, newrev)
+ # Make sure target branch was not changed during pre-receive hook
+ if current_head == oldrev
+ rugged.references.update(ref, newrev)
else
- # Update head
- current_head = find_branch(branch).target
-
- # Make sure target branch was not changed during pre-receive hook
- if current_head == oldrev
- rugged.references.update(ref, newrev)
- else
- raise CommitError.new('Commit was rejected because branch received new push')
- end
+ raise CommitError.new('Commit was rejected because branch received new push')
end
end
-
- newrev
end
+
+ newrev
end
def ls_files(ref)
@@ -1006,7 +1065,7 @@ class Repository
@avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file|
- blob_at_branch('master', file)
+ blob_at_branch(root_ref, file)
end
end
end
@@ -1014,7 +1073,7 @@ class Repository
private
def cache
- @cache ||= RepositoryCache.new(path_with_namespace)
+ @cache ||= RepositoryCache.new(path_with_namespace, @project.id)
end
def head_exists?
@@ -1026,10 +1085,14 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by { |tag| commit(tag.target).committed_date }
+ tags.sort_by { |tag| tag.target.committed_date }
end
def keep_around_ref_name(sha)
"refs/keep-around/#{sha}"
end
+
+ def repository_event(event, tags = {})
+ Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
+ end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 016172c6d7e..f4bcb49b34d 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -72,6 +72,19 @@ class SentNotification < ActiveRecord::Base
end
end
+ def position=(new_position)
+ if new_position.is_a?(String)
+ new_position = JSON.parse(new_position) rescue nil
+ end
+
+ if new_position.is_a?(Hash)
+ new_position = new_position.with_indifferent_access
+ new_position = Gitlab::Diff::Position.new(new_position)
+ end
+
+ super(new_position)
+ end
+
def to_param
self.reply_key
end
diff --git a/app/models/service.rb b/app/models/service.rb
index d7a32c28267..09b4717a523 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -17,6 +17,7 @@ class Service < ActiveRecord::Base
after_commit :reset_updated_properties
after_commit :cache_project_has_external_issue_tracker
+ after_commit :cache_project_has_external_wiki
belongs_to :project, inverse_of: :services
has_one :service_hook
@@ -25,6 +26,7 @@ class Service < ActiveRecord::Base
scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
+ scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
@@ -34,6 +36,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
@@ -76,6 +79,26 @@ class Service < ActiveRecord::Base
[]
end
+ def test_data(project, user)
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ def event_channel_names
+ []
+ end
+
+ def event_names
+ supported_events.map { |event| "#{event}_events" }
+ end
+
+ def event_field(event)
+ nil
+ end
+
+ def global_fields
+ fields
+ end
+
def supported_events
%w(push tag_push issue merge_request wiki_page)
end
@@ -94,6 +117,11 @@ class Service < ActiveRecord::Base
!project.empty_repo?
end
+ # reason why service cannot be tested
+ def disabled_title
+ "Please setup a project repository."
+ end
+
# Provide convenient accessor methods
# for each serialized property.
# Also keep track of updated properties in a similar way as ActiveModel::Dirty
@@ -203,4 +231,10 @@ class Service < ActiveRecord::Base
project.cache_has_external_issue_tracker
end
end
+
+ def cache_project_has_external_wiki
+ if project && !project.destroyed?
+ project.cache_has_external_wiki
+ end
+ end
end
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 12df68ef83b..3b8b9833565 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
user.block
user.destroy
end
+
+ def text
+ [title, description].join("\n")
+ end
end
diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb
deleted file mode 100644
index cdc7321b08e..00000000000
--- a/app/models/spam_report.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class SpamReport < ActiveRecord::Base
- belongs_to :user
-
- validates :user, presence: true
-end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index ac3fdbc7f3b..6ae9956ade5 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,14 +1,18 @@
class Todo < ActiveRecord::Base
- ASSIGNED = 1
- MENTIONED = 2
- BUILD_FAILED = 3
- MARKED = 4
+ include Sortable
+
+ ASSIGNED = 1
+ MENTIONED = 2
+ BUILD_FAILED = 3
+ MARKED = 4
+ APPROVAL_REQUIRED = 5 # This is an EE-only feature
ACTION_NAMES = {
ASSIGNED => :assigned,
MENTIONED => :mentioned,
BUILD_FAILED => :build_failed,
- MARKED => :marked
+ MARKED => :marked,
+ APPROVAL_REQUIRED => :approval_required
}
belongs_to :author, class_name: "User"
@@ -39,6 +43,23 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
+ class << self
+ def sort(method)
+ method == "priority" ? order_by_labels_priority : order_by(method)
+ end
+
+ # Order by priority depending on which issue/merge request the Todo belongs to
+ # Todos with highest priority first then oldest todos
+ # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
+ def order_by_labels_priority
+ highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
+ order('todos.created_at')
+ end
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/models/u2f_registration.rb b/app/models/u2f_registration.rb
index 00b19686d48..808acec098f 100644
--- a/app/models/u2f_registration.rb
+++ b/app/models/u2f_registration.rb
@@ -3,18 +3,19 @@
class U2fRegistration < ActiveRecord::Base
belongs_to :user
- def self.register(user, app_id, json_response, challenges)
+ def self.register(user, app_id, params, challenges)
u2f = U2F::U2F.new(app_id)
registration = self.new
begin
- response = U2F::RegisterResponse.load_from_json(json_response)
+ response = U2F::RegisterResponse.load_from_json(params[:device_response])
registration_data = u2f.register!(challenges, response)
registration.update(certificate: registration_data.certificate,
key_handle: registration_data.key_handle,
public_key: registration_data.public_key,
counter: registration_data.counter,
- user: user)
+ user: user,
+ name: params[:name])
rescue JSON::ParserError, NoMethodError, ArgumentError
registration.errors.add(:base, 'Your U2F device did not send a valid JSON response.')
rescue U2F::Error => e
diff --git a/app/models/user.rb b/app/models/user.rb
index 79c670cb35a..ad3cfbc03e4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -23,13 +23,13 @@ class User < ActiveRecord::Base
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
- key: Gitlab::Application.config.secret_key_base,
+ key: Gitlab::Application.secrets.otp_key_base,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
algorithm: 'aes-256-cbc'
devise :two_factor_authenticatable,
- otp_secret_encryption_key: Gitlab::Application.config.secret_key_base
+ otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10
serialize :otp_backup_codes, JSON
@@ -85,9 +85,10 @@ class User < ActiveRecord::Base
has_one :abuse_report, dependent: :destroy
has_many :spam_logs, dependent: :destroy
has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
+ has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline'
has_many :todos, dependent: :destroy
has_many :notification_settings, dependent: :destroy
- has_many :award_emoji, as: :awardable, dependent: :destroy
+ has_many :award_emoji, dependent: :destroy
#
# Validations
@@ -110,7 +111,7 @@ class User < ActiveRecord::Base
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
- before_validation :restricted_signup_domains, on: :create
+ before_validation :signup_domain_valid?, on: :create
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -411,6 +412,8 @@ class User < ActiveRecord::Base
end
# Returns projects user is authorized to access.
+ #
+ # If you change the logic of this method, please also update `Project#authorized_for_user`
def authorized_projects(min_access_level = nil)
Project.where("projects.id IN (#{projects_union(min_access_level).to_sql})")
end
@@ -426,6 +429,13 @@ class User < ActiveRecord::Base
owned_groups.select(:id), namespace.id).joins(:namespace)
end
+ # Returns projects which user can admin issues on (for example to move an issue to that project).
+ #
+ # This logic is duplicated from `Ability#project_abilities` into a SQL form.
+ def projects_where_can_admin_issues
+ authorized_projects(Gitlab::Access::REPORTER).non_archived.where.not(issues_enabled: false)
+ end
+
def is_admin?
admin
end
@@ -479,10 +489,10 @@ class User < ActiveRecord::Base
(personal_projects.count.to_f / projects_limit) * 100
end
- def recent_push(project_id = nil)
+ def recent_push(project_ids = nil)
# Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
- events = events.where(project_id: project_id) if project_id
+ events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently
events.recent.find do |event|
@@ -759,29 +769,6 @@ class User < ActiveRecord::Base
Project.where(id: events)
end
- def restricted_signup_domains
- email_domains = current_application_settings.restricted_signup_domains
-
- unless email_domains.blank?
- match_found = email_domains.any? do |domain|
- escaped = Regexp.escape(domain).gsub('\*', '.*?')
- regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
- email_domain = Mail::Address.new(self.email).domain
- email_domain =~ regexp
- end
-
- unless match_found
- self.errors.add :email,
- 'is not whitelisted. ' +
- 'Email domains valid for registration are: ' +
- email_domains.join(', ')
- return false
- end
- end
-
- true
- end
-
def can_be_removed?
!solo_owned_groups.present?
end
@@ -829,13 +816,13 @@ class User < ActiveRecord::Base
def todos_done_count(force: false)
Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do
- todos.done.count
+ TodosFinder.new(self, state: :done).execute.count
end
end
def todos_pending_count(force: false)
Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do
- todos.pending.count
+ TodosFinder.new(self, state: :pending).execute.count
end
end
@@ -853,7 +840,7 @@ class User < ActiveRecord::Base
groups.joins(:shared_projects).select(:project_id)]
if min_access_level
- scope = { access_level: Gitlab::Access.values.select { |access| access >= 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
@@ -880,4 +867,40 @@ class User < ActiveRecord::Base
self.can_create_group = false
self.projects_limit = 0
end
+
+ def signup_domain_valid?
+ valid = true
+ error = nil
+
+ if current_application_settings.domain_blacklist_enabled?
+ blocked_domains = current_application_settings.domain_blacklist
+ if domain_matches?(blocked_domains, self.email)
+ error = 'is not from an allowed domain.'
+ valid = false
+ end
+ end
+
+ allowed_domains = current_application_settings.domain_whitelist
+ unless allowed_domains.blank?
+ if domain_matches?(allowed_domains, self.email)
+ valid = true
+ else
+ error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
+ valid = false
+ end
+ end
+
+ self.errors.add(:email, error) unless valid
+
+ valid
+ end
+
+ def domain_matches?(email_domains, email)
+ signup_domain = Mail::Address.new(email).domain
+ email_domains.any? do |domain|
+ escaped = Regexp.escape(domain).gsub('\*', '.*?')
+ regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
+ signup_domain =~ regexp
+ end
+ end
end
diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb
new file mode 100644
index 00000000000..0949c6ef083
--- /dev/null
+++ b/app/models/user_agent_detail.rb
@@ -0,0 +1,9 @@
+class UserAgentDetail < ActiveRecord::Base
+ belongs_to :subject, polymorphic: true
+
+ validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
+
+ def submittable?
+ !submitted?
+ end
+end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 3d5fd9d3ee9..c3de278f5b7 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -44,7 +44,11 @@ class WikiPage
# The escaped URL path of this page.
def slug
- @attributes[:slug]
+ if @attributes[:slug].present?
+ @attributes[:slug]
+ else
+ wiki.wiki.preview_page(title, '', format).url_path
+ end
end
alias_method :to_param, :slug
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
new file mode 100644
index 00000000000..5c60addbe7c
--- /dev/null
+++ b/app/services/akismet_service.rb
@@ -0,0 +1,79 @@
+class AkismetService
+ attr_accessor :owner, :text, :options
+
+ def initialize(owner, text, options = {})
+ @owner = owner
+ @text = text
+ @options = options
+ end
+
+ def is_spam?
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ created_at: DateTime.now,
+ author: owner.name,
+ author_email: owner.email,
+ referrer: options[:referrer],
+ }
+
+ begin
+ is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
+ is_spam || is_blatant
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
+ false
+ end
+ end
+
+ def submit_ham
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ def submit_spam
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ private
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
+ end
+
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index e294a962352..6072123b851 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -24,10 +24,14 @@ module Auth
token[:access] = names.map do |name|
{ type: 'repository', name: name, actions: %w(*) }
end
-
+
token.encoded
end
+ def self.token_expire_at
+ Time.now + current_application_settings.container_registry_token_expire_delay.minutes
+ end
+
private
def authorized_token(*accesses)
@@ -35,7 +39,7 @@ module Auth
token.issuer = registry.issuer
token.audience = params[:service]
token.subject = current_user.try(:username)
- token.expire_time = ContainerRegistryAuthenticationService.token_expire_at
+ token.expire_time = self.class.token_expire_at
token[:access] = accesses.compact
token
end
@@ -81,9 +85,5 @@ module Auth
def registry
Gitlab.config.registry
end
-
- def self.token_expire_at
- Time.now + current_application_settings.container_registry_token_expire_delay.minutes
- end
end
end
diff --git a/app/services/boards/base_service.rb b/app/services/boards/base_service.rb
new file mode 100644
index 00000000000..b2069ca825a
--- /dev/null
+++ b/app/services/boards/base_service.rb
@@ -0,0 +1,5 @@
+module Boards
+ class BaseService < ::BaseService
+ delegate :board, to: :project
+ end
+end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
new file mode 100644
index 00000000000..072a0749285
--- /dev/null
+++ b/app/services/boards/create_service.rb
@@ -0,0 +1,16 @@
+module Boards
+ class CreateService < Boards::BaseService
+ def execute
+ create_board! unless project.board.present?
+ project.board
+ end
+
+ private
+
+ def create_board!
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
new file mode 100644
index 00000000000..34efd09ed9f
--- /dev/null
+++ b/app/services/boards/issues/list_service.rb
@@ -0,0 +1,68 @@
+module Boards
+ module Issues
+ class ListService < Boards::BaseService
+ def execute
+ issues = IssuesFinder.new(current_user, filter_params).execute
+ issues = without_board_labels(issues) unless list.movable?
+ issues = with_list_label(issues) if list.movable?
+ issues
+ end
+
+ private
+
+ def list
+ @list ||= board.lists.find(params[:id])
+ end
+
+ def filter_params
+ set_default_scope
+ set_default_sort
+ set_project
+ set_state
+
+ params
+ end
+
+ def set_default_scope
+ params[:scope] = 'all'
+ end
+
+ def set_default_sort
+ params[:sort] = 'priority'
+ end
+
+ def set_project
+ params[:project_id] = project.id
+ end
+
+ def set_state
+ params[:state] =
+ case list.list_type.to_sym
+ when :backlog then 'opened'
+ when :done then 'closed'
+ else 'all'
+ end
+ end
+
+ def board_label_ids
+ @board_label_ids ||= board.lists.movable.pluck(:label_id)
+ end
+
+ def without_board_labels(issues)
+ return issues unless board_label_ids.any?
+
+ issues.where.not(
+ LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+ .where(label_id: board_label_ids).limit(1).arel.exists
+ )
+ end
+
+ def with_list_label(issues)
+ issues.where(
+ LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
+ .where("label_links.label_id = ?", list.label_id).limit(1).arel.exists
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
new file mode 100644
index 00000000000..84dc3f70e76
--- /dev/null
+++ b/app/services/boards/issues/move_service.rb
@@ -0,0 +1,59 @@
+module Boards
+ module Issues
+ class MoveService < Boards::BaseService
+ def execute(issue)
+ return false unless can?(current_user, :update_issue, issue)
+ return false unless valid_move?
+
+ update_service.execute(issue)
+ end
+
+ private
+
+ def valid_move?
+ moving_from_list.present? && moving_to_list.present? &&
+ moving_from_list != moving_to_list
+ end
+
+ def moving_from_list
+ @moving_from_list ||= board.lists.find_by(id: params[:from_list_id])
+ end
+
+ def moving_to_list
+ @moving_to_list ||= board.lists.find_by(id: params[:to_list_id])
+ end
+
+ def update_service
+ ::Issues::UpdateService.new(project, current_user, issue_params)
+ end
+
+ def issue_params
+ {
+ add_label_ids: add_label_ids,
+ remove_label_ids: remove_label_ids,
+ state_event: issue_state
+ }
+ end
+
+ def issue_state
+ return 'reopen' if moving_from_list.done?
+ return 'close' if moving_to_list.done?
+ end
+
+ def add_label_ids
+ [moving_to_list.label_id].compact
+ end
+
+ def remove_label_ids
+ label_ids =
+ if moving_to_list.movable?
+ moving_from_list.label_id
+ else
+ board.lists.movable.pluck(:label_id)
+ end
+
+ Array(label_ids).compact
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
new file mode 100644
index 00000000000..5cb408b9d20
--- /dev/null
+++ b/app/services/boards/lists/create_service.rb
@@ -0,0 +1,22 @@
+module Boards
+ module Lists
+ class CreateService < Boards::BaseService
+ def execute
+ List.transaction do
+ create_list_at(next_position)
+ end
+ end
+
+ private
+
+ def next_position
+ max_position = board.lists.movable.maximum(:position)
+ max_position.nil? ? 0 : max_position.succ
+ end
+
+ def create_list_at(position)
+ board.lists.create(params.merge(list_type: :label, position: position))
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb
new file mode 100644
index 00000000000..25da3bfb56d
--- /dev/null
+++ b/app/services/boards/lists/destroy_service.rb
@@ -0,0 +1,25 @@
+module Boards
+ module Lists
+ class DestroyService < Boards::BaseService
+ def execute(list)
+ return false unless list.destroyable?
+
+ list.with_lock do
+ decrement_higher_lists(list)
+ remove_list(list)
+ end
+ end
+
+ private
+
+ def decrement_higher_lists(list)
+ board.lists.movable.where('position > ?', list.position)
+ .update_all('position = position - 1')
+ end
+
+ def remove_list(list)
+ list.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
new file mode 100644
index 00000000000..1c48b9786e4
--- /dev/null
+++ b/app/services/boards/lists/generate_service.rb
@@ -0,0 +1,36 @@
+module Boards
+ module Lists
+ class GenerateService < Boards::BaseService
+ def execute
+ return false unless board.lists.movable.empty?
+
+ List.transaction do
+ label_params.each { |params| create_list(params) }
+ end
+
+ true
+ end
+
+ private
+
+ def create_list(params)
+ label = find_or_create_label(params)
+ Lists::CreateService.new(project, current_user, label_id: label.id).execute
+ end
+
+ def find_or_create_label(params)
+ project.labels.create_with(color: params[:color])
+ .find_or_create_by(name: params[:name])
+ end
+
+ def label_params
+ [
+ { name: 'Development', color: '#5CB85C' },
+ { name: 'Testing', color: '#F0AD4E' },
+ { name: 'Production', color: '#FF5F00' },
+ { name: 'Ready', color: '#FF0000' }
+ ]
+ end
+ end
+ end
+end
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
new file mode 100644
index 00000000000..020ff69f4a7
--- /dev/null
+++ b/app/services/boards/lists/move_service.rb
@@ -0,0 +1,51 @@
+module Boards
+ module Lists
+ class MoveService < Boards::BaseService
+ def execute(list)
+ @old_position = list.position
+ @new_position = params[:position]
+
+ return false unless list.movable?
+ return false unless valid_move?
+
+ list.with_lock do
+ reorder_intermediate_lists
+ update_list_position(list)
+ end
+ end
+
+ private
+
+ attr_reader :old_position, :new_position
+
+ def valid_move?
+ new_position.present? && new_position != old_position &&
+ new_position >= 0 && new_position < board.lists.movable.size
+ end
+
+ def reorder_intermediate_lists
+ if old_position < new_position
+ decrement_intermediate_lists
+ else
+ increment_intermediate_lists
+ end
+ end
+
+ def decrement_intermediate_lists
+ board.lists.movable.where('position > ?', old_position)
+ .where('position <= ?', new_position)
+ .update_all('position = position - 1')
+ end
+
+ def increment_intermediate_lists
+ board.lists.movable.where('position >= ?', new_position)
+ .where('position < ?', old_position)
+ .update_all('position = position + 1')
+ end
+
+ def update_list_position(list)
+ list.update_attribute(:position, new_position)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
deleted file mode 100644
index 2dcb052d274..00000000000
--- a/app/services/ci/create_builds_service.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module Ci
- class CreateBuildsService
- def initialize(pipeline)
- @pipeline = pipeline
- @config = pipeline.config_processor
- end
-
- def execute(stage, user, status, trigger_request = nil)
- builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request)
-
- # check when to create next build
- builds_attrs = builds_attrs.select do |build_attrs|
- case build_attrs[:when]
- when 'on_success'
- status == 'success'
- when 'on_failure'
- status == 'failed'
- when 'always'
- %w(success failed).include?(status)
- end
- end
-
- # don't create the same build twice
- builds_attrs.reject! do |build_attrs|
- @pipeline.builds.find_by(ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: trigger_request,
- name: build_attrs[:name])
- end
-
- builds_attrs.map do |build_attrs|
- build_attrs.slice!(:name,
- :commands,
- :tag_list,
- :options,
- :allow_failure,
- :stage,
- :stage_idx,
- :environment)
-
- build_attrs.merge!(pipeline: @pipeline,
- ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: trigger_request,
- user: user,
- project: @pipeline.project)
-
- ##
- # We do not persist new builds here.
- # Those will be persisted when @pipeline is saved.
- #
- @pipeline.builds.new(build_attrs)
- end
- end
- end
-end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
new file mode 100644
index 00000000000..005014fa1de
--- /dev/null
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -0,0 +1,42 @@
+module Ci
+ class CreatePipelineBuildsService < BaseService
+ attr_reader :pipeline
+
+ def execute(pipeline)
+ @pipeline = pipeline
+
+ new_builds.map do |build_attributes|
+ create_build(build_attributes)
+ end
+ end
+
+ private
+
+ def create_build(build_attributes)
+ build_attributes = build_attributes.merge(
+ pipeline: pipeline,
+ project: pipeline.project,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ user: current_user,
+ trigger_request: trigger_request
+ )
+ pipeline.builds.create(build_attributes)
+ end
+
+ def new_builds
+ @new_builds ||= pipeline.config_builds_attributes.
+ reject { |build| existing_build_names.include?(build[:name]) }
+ end
+
+ def existing_build_names
+ @existing_build_names ||= pipeline.builds.pluck(:name)
+ end
+
+ def trigger_request
+ return @trigger_request if defined?(@trigger_request)
+
+ @trigger_request ||= pipeline.trigger_requests.first
+ end
+ end
+end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index b1ee6874190..cde856b0186 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -1,48 +1,101 @@
module Ci
class CreatePipelineService < BaseService
- def execute
- pipeline = project.pipelines.new(params)
+ attr_reader :pipeline
- unless ref_names.include?(params[:ref])
- pipeline.errors.add(:base, 'Reference not found')
- return pipeline
+ def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil)
+ @pipeline = Ci::Pipeline.new(
+ project: project,
+ ref: ref,
+ sha: sha,
+ before_sha: before_sha,
+ tag: tag?,
+ trigger_requests: Array(trigger_request),
+ user: current_user
+ )
+
+ unless project.builds_enabled?
+ return error('Pipeline is disabled')
end
- if commit
- pipeline.sha = commit.id
- else
- pipeline.errors.add(:base, 'Commit not found')
- return pipeline
+ unless trigger_request || can?(current_user, :create_pipeline, project)
+ return error('Insufficient permissions to create a new pipeline')
end
- unless can?(current_user, :create_pipeline, project)
- pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline')
- return pipeline
+ unless branch? || tag?
+ return error('Reference not found')
+ end
+
+ unless commit
+ return error('Commit not found')
end
unless pipeline.config_processor
- pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file')
- return pipeline
+ unless pipeline.ci_yaml_file
+ return error('Missing .gitlab-ci.yml file')
+ end
+ return error(pipeline.yaml_errors, save: save_on_errors)
end
- pipeline.save!
+ if !ignore_skip_ci && skip_ci?
+ pipeline.skip if save_on_errors
+ return pipeline
+ end
- unless pipeline.create_builds(current_user)
- pipeline.errors.add(:base, 'No builds for this pipeline.')
+ unless pipeline.config_builds_attributes.present?
+ return error('No builds for this pipeline.')
end
pipeline.save
+ pipeline.process!
pipeline
end
private
- def ref_names
- @ref_names ||= project.repository.ref_names
+ def skip_ci?
+ pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message
end
def commit
- @commit ||= project.commit(params[:ref])
+ @commit ||= project.commit(origin_sha || origin_ref)
+ end
+
+ def sha
+ commit.try(:id)
+ end
+
+ def before_sha
+ params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
+ end
+
+ def origin_sha
+ params[:checkout_sha] || params[:after]
+ end
+
+ def origin_ref
+ params[:ref]
+ end
+
+ def branch?
+ project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
+ end
+
+ def tag?
+ project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+ end
+
+ def ref
+ Gitlab::Git.ref_name(origin_ref)
+ end
+
+ def valid_sha?
+ origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
+ end
+
+ def error(message, save: false)
+ pipeline.errors.add(:base, message)
+ pipeline.drop if save
+ pipeline
end
end
end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 1e629cf119a..6af3c1ca5b1 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,20 +1,11 @@
module Ci
class CreateTriggerRequestService
def execute(project, trigger, ref, variables = nil)
- commit = project.commit(ref)
- return unless commit
+ trigger_request = trigger.trigger_requests.create(variables: variables)
- # check if ref is tag
- tag = project.repository.find_tag(ref).present?
-
- pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag)
-
- trigger_request = trigger.trigger_requests.create!(
- variables: variables,
- pipeline: pipeline,
- )
-
- if pipeline.create_builds(nil, trigger_request)
+ pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref).
+ execute(ignore_skip_ci: true, trigger_request: trigger_request)
+ if pipeline.persisted?
trigger_request
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
new file mode 100644
index 00000000000..f049ed628db
--- /dev/null
+++ b/app/services/ci/process_pipeline_service.rb
@@ -0,0 +1,77 @@
+module Ci
+ class ProcessPipelineService < BaseService
+ attr_reader :pipeline
+
+ 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
+
+ new_builds =
+ stage_indexes_of_created_builds.map do |index|
+ process_stage(index)
+ end
+
+ # Return a flag if a when builds got enqueued
+ new_builds.flatten.any?
+ end
+
+ private
+
+ def create_builds!
+ Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline)
+ end
+
+ def process_stage(index)
+ current_status = status_for_prior_stages(index)
+
+ created_builds_in_stage(index).select do |build|
+ process_build(build, current_status)
+ end
+ end
+
+ def process_build(build, current_status)
+ return false unless HasStatus::COMPLETED_STATUSES.include?(current_status)
+
+ if valid_statuses_for_when(build.when).include?(current_status)
+ build.enqueue
+ true
+ else
+ build.skip
+ false
+ end
+ end
+
+ def valid_statuses_for_when(value)
+ case value
+ when 'on_success'
+ %w[success]
+ when 'on_failure'
+ %w[failed]
+ when 'always'
+ %w[success failed]
+ else
+ []
+ end
+ end
+
+ def status_for_prior_stages(index)
+ pipeline.builds.where('stage_idx < ?', index).latest.status || 'success'
+ end
+
+ def stage_indexes_of_created_builds
+ created_builds.order(:stage_idx).pluck('distinct stage_idx')
+ end
+
+ def created_builds_in_stage(index)
+ created_builds.where(stage_idx: index)
+ end
+
+ def created_builds
+ pipeline.builds.created
+ end
+ end
+end
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index c578097376a..ed73d8cb8c2 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -23,7 +23,7 @@ module Commits
private
def check_push_permissions
- allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
+ allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise ValidationError.new('You are not allowed to push into this branch')
@@ -31,7 +31,7 @@ module Commits
true
end
-
+
def create_target_branch(new_branch)
# Temporary branch exists and contains the change commit
return success if repository.find_branch(new_branch)
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index e2bccbdbcc3..6d6075628af 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::Git::Compare object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
+ def execute(source_project, source_branch, target_project, target_branch)
source_commit = source_project.commit(source_branch)
return unless source_commit
@@ -20,10 +20,12 @@ class CompareService
)
end
- Gitlab::Git::Compare.new(
+ raw_compare = Gitlab::Git::Compare.new(
target_project.repository.raw_repository,
target_branch,
- source_sha,
+ source_sha
)
+
+ Compare.new(raw_compare, target_project)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index d874582d54f..757fc35a78f 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -15,21 +15,19 @@ class CreateBranchService < BaseService
return error('Branch already exists')
end
- new_branch = nil
-
- if source_project != @project
- repository.with_tmp_ref do |tmp_ref|
- repository.fetch_ref(
- source_project.repository.path_to_repo,
- "refs/heads/#{ref}",
- tmp_ref
- )
-
- new_branch = repository.add_branch(current_user, branch_name, tmp_ref)
- end
- else
- new_branch = repository.add_branch(current_user, branch_name, ref)
- end
+ new_branch = if source_project != @project
+ repository.fetch_ref(
+ source_project.repository.path_to_repo,
+ "refs/heads/#{ref}",
+ "refs/heads/#{branch_name}"
+ )
+
+ repository.after_create_branch
+
+ repository.find_branch(branch_name)
+ else
+ repository.add_branch(current_user, branch_name, ref)
+ end
if new_branch
success(new_branch)
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
deleted file mode 100644
index f947e8f452e..00000000000
--- a/app/services/create_commit_builds_service.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-class CreateCommitBuildsService
- def execute(project, user, params)
- return unless project.builds_enabled?
-
- before_sha = params[:checkout_sha] || params[:before]
- sha = params[:checkout_sha] || params[:after]
- origin_ref = params[:ref]
-
- ref = Gitlab::Git.ref_name(origin_ref)
- tag = Gitlab::Git.tag_ref?(origin_ref)
-
- # Skip branch removal
- if sha == Gitlab::Git::BLANK_SHA
- return false
- end
-
- @pipeline = Ci::Pipeline.new(project: project, sha: sha, ref: ref, before_sha: before_sha, tag: tag)
-
- ##
- # Skip creating pipeline if no gitlab-ci.yml is found
- #
- unless @pipeline.ci_yaml_file
- return false
- end
-
- ##
- # Skip creating builds for commits that have [ci skip]
- # but save pipeline object
- #
- if @pipeline.skip_ci?
- return save_pipeline!
- end
-
- ##
- # Skip creating builds when CI config is invalid
- # but save pipeline object
- #
- unless @pipeline.config_processor
- return save_pipeline!
- end
-
- ##
- # Skip creating pipeline object if there are no builds for it.
- #
- unless @pipeline.create_builds(user)
- @pipeline.errors.add(:base, 'No builds created')
- return false
- end
-
- save_pipeline!
- end
-
- private
-
- ##
- # Create a new pipeline and touch object to calculate status
- #
- def save_pipeline!
- @pipeline.save!
- @pipeline.touch
- @pipeline
- end
-end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
deleted file mode 100644
index 59a66fde47a..00000000000
--- a/app/services/create_spam_log_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class CreateSpamLogService < BaseService
- def initialize(project, user, params)
- super(project, user, params)
- end
-
- def execute
- spam_params = params.merge({ user_id: @current_user.id,
- project_id: @project.id } )
- spam_log = SpamLog.new(spam_params)
- spam_log.save
- spam_log
- end
-end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 332c55581a1..918eddaa53a 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -39,7 +39,12 @@ class DeleteBranchService < BaseService
end
def build_push_data(branch)
- Gitlab::PushDataBuilder
- .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ branch.target.sha,
+ Gitlab::Git::BLANK_SHA,
+ "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
+ [])
end
end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 1e41fbe34b6..d0cb151a010 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -33,7 +33,12 @@ class DeleteTagService < BaseService
end
def build_push_data(tag)
- Gitlab::PushDataBuilder
- .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ tag.target.sha,
+ Gitlab::Git::BLANK_SHA,
+ "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
+ [])
end
end
diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb
index ce79287e35a..eaff88d6463 100644
--- a/app/services/delete_user_service.rb
+++ b/app/services/delete_user_service.rb
@@ -18,9 +18,14 @@ class DeleteUserService
user.personal_projects.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
end
- user.destroy
+ # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
+ namespace = user.namespace
+ user_data = user.destroy
+ namespace.really_destroy!
+
+ user_data
end
end
diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb
index 3c42ac61be4..0081364b8aa 100644
--- a/app/services/destroy_group_service.rb
+++ b/app/services/destroy_group_service.rb
@@ -5,13 +5,23 @@ class DestroyGroupService
@group, @current_user = group, user
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
+ end
+
def execute
group.projects.each do |project|
+ # Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete!
+ # that contain all these repositories
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
- group.destroy
+ group.really_destroy!
end
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 37c5e321b39..ea94818713b 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -9,12 +9,15 @@ module Files
@commit_message = params[:commit_message]
@file_path = params[:file_path]
+ @previous_path = params[:previous_path]
@file_content = if params[:file_content_encoding] == 'base64'
Base64.decode64(params[:file_content])
else
params[:file_content]
end
+ @last_commit_sha = params[:last_commit_sha]
+ # Validate parameters
validate
# Create new branch if it different from source_branch
@@ -42,7 +45,7 @@ module Files
end
def validate
- allowed = ::Gitlab::GitAccess.new(current_user, project, 'web').can_push_to_branch?(@target_branch)
+ allowed = ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(@target_branch)
unless allowed
raise_error("You are not allowed to push into this branch")
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 1960dc7d949..4fc3b640799 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -2,8 +2,34 @@ require_relative "base_service"
module Files
class UpdateService < Files::BaseService
+ class FileChangedError < StandardError; end
+
def commit
- repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, true)
+ repository.update_file(current_user, @file_path, @file_content,
+ branch: @target_branch,
+ previous_path: @previous_path,
+ message: @commit_message)
+ end
+
+ private
+
+ def validate
+ super
+
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file that has changed since you started editing it.")
+ end
+ end
+
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
+
+ @last_commit_sha != last_commit.sha
+ end
+
+ def last_commit
+ @last_commit ||= Gitlab::Git::Commit.
+ last_for_path(@source_project.repository, @source_branch, @file_path)
end
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index a886f35981f..78feb37aa2a 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -69,7 +69,7 @@ class GitPushService < BaseService
SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
- CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
+ Ci::CreatePipelineService.new(project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
end
@@ -88,8 +88,18 @@ class GitPushService < BaseService
# Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE
- developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
- @project.protected_branches.create({ name: @project.default_branch, developers_can_push: developers_can_push })
+
+ params = {
+ name: @project.default_branch,
+ push_access_levels_attributes: [{
+ access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }],
+ merge_access_levels_attributes: [{
+ access_level: current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }]
+ }
+
+ ProtectedBranches::CreateService.new(@project, current_user, params).execute
end
end
@@ -128,13 +138,23 @@ class GitPushService < BaseService
end
def build_push_data
- @push_data ||= Gitlab::PushDataBuilder.
- build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
+ @push_data ||= Gitlab::DataBuilder::Push.build(
+ @project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ push_commits)
end
def build_push_data_system_hook
- @push_data_system ||= Gitlab::PushDataBuilder.
- build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
+ @push_data_system ||= Gitlab::DataBuilder::Push.build(
+ @project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ [])
end
def push_to_existing_branch?
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 58573078048..e6002b03b93 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -11,7 +11,7 @@ class GitTagPushService < BaseService
SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks)
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
- CreateCommitBuildsService.new.execute(project, current_user, @push_data)
+ Ci::CreatePipelineService.new(project, current_user, @push_data).execute
ProjectCacheWorker.perform_async(project.id)
true
@@ -26,20 +26,32 @@ class GitTagPushService < BaseService
unless Gitlab::Git.blank_ref?(params[:newrev])
tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
-
- if tag && tag.target == params[:newrev]
+
+ if tag && tag.object_sha == params[:newrev]
commit = project.commit(tag.target)
commits = [commit].compact
message = tag.message
end
end
- Gitlab::PushDataBuilder.
- build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ commits,
+ message)
end
def build_system_push_data
- Gitlab::PushDataBuilder.
- build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
+ Gitlab::DataBuilder::Push.build(
+ project,
+ current_user,
+ params[:oldrev],
+ params[:newrev],
+ params[:ref],
+ [],
+ '')
end
end
diff --git a/app/services/ham_service.rb b/app/services/ham_service.rb
new file mode 100644
index 00000000000..b0e1799b489
--- /dev/null
+++ b/app/services/ham_service.rb
@@ -0,0 +1,26 @@
+class HamService
+ attr_accessor :spam_log
+
+ def initialize(spam_log)
+ @spam_log = spam_log
+ end
+
+ def mark_as_ham!
+ if akismet.submit_ham
+ spam_log.update_attribute(:submitted_as_ham, true)
+ else
+ false
+ end
+ end
+
+ private
+
+ def akismet
+ @akismet ||= AkismetService.new(
+ spam_log.user,
+ spam_log.text,
+ ip_address: spam_log.source_ip,
+ user_agent: spam_log.user_agent
+ )
+ end
+end
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
new file mode 100644
index 00000000000..6442406d77e
--- /dev/null
+++ b/app/services/import_export_clean_up_service.rb
@@ -0,0 +1,24 @@
+class ImportExportCleanUpService
+ LAST_MODIFIED_TIME_IN_MINUTES = 1440
+
+ attr_reader :mmin, :path
+
+ def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES)
+ @mmin = mmin
+ @path = Gitlab::ImportExport.storage_path
+ end
+
+ def execute
+ Gitlab::Metrics.measure(:import_export_clean_up) do
+ return unless File.directory?(path)
+
+ clean_up_export_files
+ end
+ end
+
+ private
+
+ def clean_up_export_files
+ Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e3dc569152c..e06c37c323e 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -69,14 +69,9 @@ class IssuableBaseService < BaseService
end
def filter_labels
- if params[:add_label_ids].present? || params[:remove_label_ids].present?
- params.delete(:label_ids)
-
- filter_labels_in_param(:add_label_ids)
- filter_labels_in_param(:remove_label_ids)
- else
- filter_labels_in_param(:label_ids)
- end
+ filter_labels_in_param(:add_label_ids)
+ filter_labels_in_param(:remove_label_ids)
+ filter_labels_in_param(:label_ids)
end
def filter_labels_in_param(key)
@@ -85,29 +80,90 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id)
end
- def update_issuable(issuable, attributes)
+ def process_label_ids(attributes, existing_label_ids: nil)
+ label_ids = attributes.delete(:label_ids)
+ add_label_ids = attributes.delete(:add_label_ids)
+ remove_label_ids = attributes.delete(:remove_label_ids)
+
+ new_label_ids = existing_label_ids || label_ids || []
+
+ if add_label_ids.blank? && remove_label_ids.blank?
+ new_label_ids = label_ids if label_ids
+ else
+ new_label_ids |= add_label_ids if add_label_ids
+ new_label_ids -= remove_label_ids if remove_label_ids
+ end
+
+ new_label_ids
+ end
+
+ def merge_slash_commands_into_params!(issuable)
+ description, command_params =
+ SlashCommands::InterpretService.new(project, current_user).
+ execute(params[:description], issuable)
+
+ params[:description] = description
+
+ params.merge!(command_params)
+ end
+
+ def create_issuable(issuable, attributes, label_ids:)
issuable.with_transaction_returning_status do
- add_label_ids = attributes.delete(:add_label_ids)
- remove_label_ids = attributes.delete(:remove_label_ids)
+ if issuable.save
+ issuable.update_attributes(label_ids: label_ids)
+ end
+ end
+ end
+
+ def create(issuable)
+ merge_slash_commands_into_params!(issuable)
+ filter_params
- issuable.label_ids |= add_label_ids if add_label_ids
- issuable.label_ids -= remove_label_ids if remove_label_ids
+ params.delete(:state_event)
+ params[:author] ||= current_user
+ label_ids = process_label_ids(params)
- issuable.assign_attributes(attributes.merge(updated_by: current_user))
+ issuable.assign_attributes(params)
- issuable.save
+ before_create(issuable)
+
+ if params.present? && create_issuable(issuable, params, label_ids: label_ids)
+ after_create(issuable)
+ issuable.create_cross_references!(current_user)
+ execute_hooks(issuable)
+ end
+
+ issuable
+ end
+
+ def before_create(issuable)
+ # To be overridden by subclasses
+ end
+
+ def after_create(issuable)
+ # To be overridden by subclasses
+ end
+
+ def update_issuable(issuable, attributes)
+ issuable.with_transaction_returning_status do
+ issuable.update(attributes.merge(updated_by: current_user))
end
end
def update(issuable)
change_state(issuable)
+ change_subscription(issuable)
+ change_todo(issuable)
filter_params
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)
if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
handle_common_system_notes(issuable, old_labels: old_labels)
- handle_changes(issuable, old_labels: old_labels)
+ handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
end
@@ -124,6 +180,25 @@ class IssuableBaseService < BaseService
end
end
+ def change_subscription(issuable)
+ case params.delete(:subscription_event)
+ when 'subscribe'
+ issuable.subscribe(current_user)
+ when 'unsubscribe'
+ issuable.unsubscribe(current_user)
+ end
+ end
+
+ def change_todo(issuable)
+ case params.delete(:todo_event)
+ when 'add'
+ todo_service.mark_todo(issuable, current_user)
+ when 'done'
+ todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
+ todo_service.mark_todos_as_done([todo], current_user) if todo
+ end
+ end
+
def has_changes?(issuable, old_labels: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
index cd08c3a0cb9..7e19a73f71a 100644
--- a/app/services/issues/bulk_update_service.rb
+++ b/app/services/issues/bulk_update_service.rb
@@ -4,7 +4,7 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",")
issue_params = params
- %i(state_event milestone_id assignee_id add_label_ids remove_label_ids).each do |key|
+ %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
issue_params.delete(key) unless issue_params[key].present?
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 859c934ea3b..45cca216ccc 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,8 @@
module Issues
class CloseService < Issues::BaseService
def execute(issue, commit: nil, notifications: true, system_note: true)
+ return issue unless can?(current_user, :update_issue, issue)
+
if project.jira_tracker? && project.jira_service.active
project.jira_service.execute(commit, issue)
todo_service.close_issue(issue, current_user)
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index e63e1af8766..ea1690f3e38 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -1,21 +1,33 @@
module Issues
class CreateService < Issues::BaseService
def execute
- filter_params
- label_params = params[:label_ids]
- issue = project.issues.new(params.except(:label_ids))
- issue.author = params[:author] || current_user
+ @request = params.delete(:request)
+ @api = params.delete(:api)
- if issue.save
- issue.update_attributes(label_ids: label_params)
- notification_service.new_issue(issue, current_user)
- todo_service.new_issue(issue, current_user)
- event_service.open_issue(issue, current_user)
- issue.create_cross_references!(current_user)
- execute_hooks(issue, 'open')
- end
+ @issue = project.issues.new
- issue
+ create(@issue)
+ end
+
+ def before_create(issuable)
+ issuable.spam = spam_service.check(@api)
+ end
+
+ def after_create(issuable)
+ event_service.open_issue(issuable, current_user)
+ notification_service.new_issue(issuable, current_user)
+ todo_service.new_issue(issuable, current_user)
+ user_agent_detail_service.create
+ end
+
+ private
+
+ def spam_service
+ SpamService.new(@issue, @request)
+ end
+
+ def user_agent_detail_service
+ UserAgentDetailService.new(@issue, @request)
end
end
end
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index e48ca359f4f..40fbe354492 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -1,6 +1,8 @@
module Issues
class ReopenService < Issues::BaseService
def execute(issue)
+ return issue unless can?(current_user, :update_issue, issue)
+
if issue.reopen
event_service.reopen_issue(issue, current_user)
create_note(issue)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c7d406cc331..a2111b3806b 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -4,7 +4,7 @@ module Issues
update(issue)
end
- def handle_changes(issue, old_labels: [])
+ def handle_changes(issue, old_labels: [], old_mentioned_users: [])
if has_changes?(issue, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(issue, current_user)
end
@@ -32,6 +32,11 @@ module Issues
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
+
+ added_mentions = issue.mentioned_users - old_mentioned_users
+ if added_mentions.present?
+ notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
+ end
end
def reopen_service
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 00000000000..ca9db59cac7
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+module Members
+ class AuthorizedDestroyService < BaseService
+ attr_accessor :member, :user
+
+ def initialize(member, user = nil)
+ @member, @user = member, user
+ end
+
+ def execute
+ return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ member.destroy
+
+ if member.request? && member.user != user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 15358f80208..9a2bf82ef51 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -2,20 +2,16 @@ module Members
class DestroyService < BaseService
attr_accessor :member, :current_user
- def initialize(member, user)
- @member, @current_user = member, user
+ def initialize(member, current_user)
+ @member = member
+ @current_user = current_user
end
def execute
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
-
- member.destroy
-
- if member.request? && member.user != current_user
- notification_service.decline_access_request(member)
- end
+ AuthorizedDestroyService.new(member, current_user).execute
end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index bc3606a14c2..ba424b09463 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -17,16 +17,19 @@ module MergeRequests
end
end
- def hook_data(merge_request, action)
+ def hook_data(merge_request, action, oldrev = nil)
hook_data = merge_request.to_hook_data(current_user)
hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request)
hook_data[:object_attributes][:action] = action
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
+ hook_data[:object_attributes][:oldrev] = oldrev
+ end
hook_data
end
- def execute_hooks(merge_request, action = 'open')
+ def execute_hooks(merge_request, action = 'open', oldrev = nil)
if merge_request.project
- merge_data = hook_data(merge_request, action)
+ merge_data = hook_data(merge_request, action, oldrev)
merge_request.project.execute_hooks(merge_data, :merge_request_hooks)
merge_request.project.execute_services(merge_data, :merge_request_hooks)
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 7fe57747265..290742f1506 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -34,7 +34,7 @@ module MergeRequests
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
- merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project)
+ merge_request.compare_commits = commits
merge_request.can_be_created = true
merge_request.compare = compare
else
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index 27ee81fe3e7..f2053bda83a 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -1,6 +1,8 @@
module MergeRequests
class CloseService < MergeRequests::BaseService
def execute(merge_request, commit = nil)
+ return merge_request unless can?(current_user, :update_merge_request, merge_request)
+
# If we close MergeRequest we want to ignore validation
# so we can close broken one (Ex. fork project removed)
merge_request.allow_broken = true
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 96a25330af1..73247e62421 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -7,26 +7,19 @@ module MergeRequests
source_project = @project
@project = Project.find(params[:target_project_id]) if params[:target_project_id]
- filter_params
- label_params = params.delete(:label_ids)
- force_remove_source_branch = params.delete(:force_remove_source_branch)
+ params[:target_project_id] ||= source_project.id
- merge_request = MergeRequest.new(params)
+ merge_request = MergeRequest.new
merge_request.source_project = source_project
- merge_request.target_project ||= source_project
- merge_request.author = current_user
- merge_request.merge_params['force_remove_source_branch'] = force_remove_source_branch
+ merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
- if merge_request.save
- merge_request.update_attributes(label_ids: label_params)
- event_service.open_mr(merge_request, current_user)
- notification_service.new_merge_request(merge_request, current_user)
- todo_service.new_merge_request(merge_request, current_user)
- merge_request.create_cross_references!(current_user)
- execute_hooks(merge_request)
- end
+ create(merge_request)
+ end
- merge_request
+ def after_create(issuable)
+ event_service.open_mr(issuable, current_user)
+ notification_service.new_merge_request(issuable, current_user)
+ todo_service.new_merge_request(issuable, current_user)
end
end
end
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
new file mode 100644
index 00000000000..08c1f72d65a
--- /dev/null
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -0,0 +1,63 @@
+module MergeRequests
+ class GetUrlsService < BaseService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute(changes)
+ branches = get_branches(changes)
+ merge_requests_map = opened_merge_requests_from_source_branches(branches)
+ branches.map do |branch|
+ existing_merge_request = merge_requests_map[branch]
+ if existing_merge_request
+ url_for_existing_merge_request(existing_merge_request)
+ else
+ url_for_new_merge_request(branch)
+ end
+ end
+ end
+
+ private
+
+ def opened_merge_requests_from_source_branches(branches)
+ merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
+ merge_requests.inject({}) do |hash, mr|
+ hash[mr.source_branch] = mr
+ hash
+ end
+ end
+
+ def get_branches(changes)
+ return [] if project.empty_repo?
+ return [] unless project.merge_requests_enabled
+
+ changes_list = Gitlab::ChangesList.new(changes)
+ changes_list.map do |change|
+ next unless Gitlab::Git.branch_ref?(change[:ref])
+
+ # Deleted branch
+ next if Gitlab::Git.blank_ref?(change[:newrev])
+
+ # Default branch
+ branch_name = Gitlab::Git.branch_name(change[:ref])
+ next if branch_name == project.default_branch
+
+ branch_name
+ end.compact
+ end
+
+ def url_for_new_merge_request(branch_name)
+ merge_request_params = { source_branch: branch_name }
+ url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params)
+ { branch_name: branch_name, url: url, new_merge_request: true }
+ end
+
+ def url_for_existing_merge_request(merge_request)
+ target_project = merge_request.target_project
+ url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request)
+ { branch_name: merge_request.source_branch, url: url, new_merge_request: false }
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb
new file mode 100644
index 00000000000..2945a7fd4e4
--- /dev/null
+++ b/app/services/merge_requests/merge_request_diff_cache_service.rb
@@ -0,0 +1,8 @@
+module MergeRequests
+ class MergeRequestDiffCacheService
+ def execute(merge_request)
+ # Executing the iteration we cache all the highlighted diff information
+ merge_request.diffs.diff_files.to_a
+ end
+ end
+end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index f1b1d90c457..b037780c431 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -34,8 +34,14 @@ module MergeRequests
committer: committer
}
- commit_id = repository.merge(current_user, merge_request.diff_head_sha, merge_request.target_branch, options)
- merge_request.update(merge_commit_sha: commit_id)
+ commit_id = repository.merge(current_user, merge_request, options)
+
+ if commit_id
+ merge_request.update(merge_commit_sha: commit_id)
+ else
+ merge_request.update(merge_error: 'Conflicts detected during merge')
+ false
+ end
rescue GitHooksService::PreReceiveError => e
merge_request.update(merge_error: e.message)
false
@@ -43,6 +49,8 @@ module MergeRequests
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
false
+ ensure
+ merge_request.update(in_progress_merge_commit_sha: nil)
end
def after_merge
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index b11ecd97a57..5cedd6f11d9 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -48,7 +48,7 @@ module MergeRequests
end
def force_push?
- Gitlab::ForcePushCheck.force_push?(@project, @oldrev, @newrev)
+ Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
end
# Refresh merge request diff if we push to source or target branch of merge request
@@ -137,7 +137,7 @@ module MergeRequests
# Call merge request webhook with update branches
def execute_mr_web_hooks
merge_requests_for_source_branch.each do |merge_request|
- execute_hooks(merge_request, 'update')
+ execute_hooks(merge_request, 'update', @oldrev)
end
end
diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb
index eb88ae9d11c..fadcce5d9b6 100644
--- a/app/services/merge_requests/reopen_service.rb
+++ b/app/services/merge_requests/reopen_service.rb
@@ -1,6 +1,8 @@
module MergeRequests
class ReopenService < MergeRequests::BaseService
def execute(merge_request)
+ return merge_request unless can?(current_user, :update_merge_request, merge_request)
+
if merge_request.reopen
event_service.reopen_mr(merge_request, current_user)
create_note(merge_request)
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
new file mode 100644
index 00000000000..adc71b0c2bc
--- /dev/null
+++ b/app/services/merge_requests/resolve_service.rb
@@ -0,0 +1,31 @@
+module MergeRequests
+ class ResolveService < MergeRequests::BaseService
+ attr_accessor :conflicts, :rugged, :merge_index
+
+ def execute(merge_request)
+ @conflicts = merge_request.conflicts
+ @rugged = project.repository.rugged
+ @merge_index = conflicts.merge_index
+
+ conflicts.files.each do |file|
+ write_resolved_file_to_index(file, params[:sections])
+ end
+
+ commit_params = {
+ message: params[:commit_message] || conflicts.default_commit_message,
+ parents: [conflicts.our_commit, conflicts.their_commit].map(&:oid),
+ tree: merge_index.write_tree(rugged)
+ }
+
+ project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params)
+ end
+
+ def write_resolved_file_to_index(file, resolutions)
+ new_file = file.resolve_lines(resolutions).map(&:text).join("\n")
+ our_path = file.our_path
+
+ merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
+ merge_index.conflict_remove(our_path)
+ end
+ end
+end
diff --git a/app/services/merge_requests/resolved_discussion_notification_service.rb b/app/services/merge_requests/resolved_discussion_notification_service.rb
new file mode 100644
index 00000000000..3a09350c847
--- /dev/null
+++ b/app/services/merge_requests/resolved_discussion_notification_service.rb
@@ -0,0 +1,10 @@
+module MergeRequests
+ class ResolvedDiscussionNotificationService < MergeRequests::BaseService
+ def execute(merge_request)
+ return unless merge_request.discussions_resolved?
+
+ SystemNoteService.resolve_all_discussions(merge_request, project, current_user)
+ notification_service.resolve_all_discussions(merge_request, current_user)
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 026a37997d4..30c5f24988c 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -16,7 +16,7 @@ module MergeRequests
update(merge_request)
end
- def handle_changes(merge_request, old_labels: [])
+ def handle_changes(merge_request, old_labels: [], old_mentioned_users: [])
if has_changes?(merge_request, old_labels: old_labels)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -55,6 +55,15 @@ module MergeRequests
current_user
)
end
+
+ added_mentions = merge_request.mentioned_users - old_mentioned_users
+ if added_mentions.present?
+ notification_service.new_mentions_in_merge_request(
+ merge_request,
+ added_mentions,
+ current_user
+ )
+ end
end
def reopen_service
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 02fca5c0ea3..a36008c3ef5 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,14 +8,36 @@ module Notes
if note.award_emoji?
noteable = note.noteable
todo_service.new_award_emoji(noteable, current_user)
-
return noteable.create_award_emoji(note.award_emoji_name, current_user)
end
- if note.save
+ # We execute commands (extracted from `params[:note]`) on the noteable
+ # **before** we save the note because if the note consists of commands
+ # only, there is no need be create a note!
+ slash_commands_service = SlashCommandsService.new(project, current_user)
+
+ if slash_commands_service.supported?(note)
+ content, command_params = slash_commands_service.extract_commands(note)
+
+ only_commands = content.empty?
+
+ note.note = content
+ end
+
+ if !only_commands && note.save
# Finish the harder work in the background
NewNoteWorker.perform_in(2.seconds, note.id, params)
- TodoService.new.new_note(note, current_user)
+ todo_service.new_note(note, current_user)
+ end
+
+ if command_params && command_params.any?
+ slash_commands_service.execute(command_params, note)
+
+ # We must add the error after we call #save because errors are reset
+ # when #save is called
+ if only_commands
+ note.errors.add(:commands_only, 'Your commands have been executed!')
+ end
end
note
diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb
index 534c48aefff..e4cd3fc7833 100644
--- a/app/services/notes/post_process_service.rb
+++ b/app/services/notes/post_process_service.rb
@@ -16,7 +16,7 @@ module Notes
end
def hook_data
- Gitlab::NoteDataBuilder.build(@note, @note.author)
+ Gitlab::DataBuilder::Note.build(@note, @note.author)
end
def execute_note_hooks
diff --git a/app/services/notes/slash_commands_service.rb b/app/services/notes/slash_commands_service.rb
new file mode 100644
index 00000000000..4a9a8a64653
--- /dev/null
+++ b/app/services/notes/slash_commands_service.rb
@@ -0,0 +1,33 @@
+module Notes
+ class SlashCommandsService < BaseService
+ UPDATE_SERVICES = {
+ 'Issue' => Issues::UpdateService,
+ 'MergeRequest' => MergeRequests::UpdateService
+ }
+
+ def supported?(note)
+ noteable_update_service(note) &&
+ can?(current_user, :"update_#{note.noteable_type.underscore}", note.noteable)
+ end
+
+ def extract_commands(note)
+ return [note.note, {}] unless supported?(note)
+
+ SlashCommands::InterpretService.new(project, current_user).
+ execute(note.note, note.noteable)
+ end
+
+ def execute(command_params, note)
+ return if command_params.empty?
+ return unless supported?(note)
+
+ noteable_update_service(note).new(project, current_user, command_params).execute(note.noteable)
+ end
+
+ private
+
+ def noteable_update_service(note)
+ UPDATE_SERVICES[note.noteable_type]
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index ab6e51209ee..6139ed56e25 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -35,6 +35,20 @@ class NotificationService
new_resource_email(issue, issue.project, :new_issue_email)
end
+ # When issue text is updated, we should send an email to:
+ #
+ # * newly mentioned project team members with notification level higher than Participating
+ #
+ def new_mentions_in_issue(issue, new_mentioned_users, current_user)
+ new_mentions_in_resource_email(
+ issue,
+ issue.project,
+ new_mentioned_users,
+ current_user,
+ :new_mention_in_issue_email
+ )
+ end
+
# When we close an issue we should send an email to:
#
# * issue author if their notification level is not Disabled
@@ -75,6 +89,20 @@ class NotificationService
new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
end
+ # When merge request text is updated, we should send an email to:
+ #
+ # * newly mentioned project team members with notification level higher than Participating
+ #
+ def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_user)
+ new_mentions_in_resource_email(
+ merge_request,
+ merge_request.target_project,
+ new_mentioned_users,
+ current_user,
+ :new_mention_in_merge_request_email
+ )
+ end
+
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
@@ -120,6 +148,14 @@ class NotificationService
)
end
+ def resolve_all_discussions(merge_request, current_user)
+ recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+
+ recipients.each do |recipient|
+ mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
+ end
+ end
+
# Notify new user with email after creation
def new_user(user, token = nil)
# Don't email omniauth created users
@@ -177,7 +213,7 @@ class NotificationService
# build notify method like 'note_commit_email'
notify_method = "note_#{note.noteable_type.underscore}_email".to_sym
-
+
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -206,7 +242,6 @@ class NotificationService
project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
- project_member.access_level,
project_member.created_by_id
).deliver_later
end
@@ -233,7 +268,6 @@ class NotificationService
group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
- group_member.access_level,
group_member.created_by_id
).deliver_later
end
@@ -471,6 +505,15 @@ class NotificationService
end
end
+ def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
+ recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = recipients & new_mentioned_users
+
+ recipients.each do |recipient|
+ mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
+ end
+ end
+
def close_resource_email(target, project, current_user, method)
action = method == :merged_merge_request_email ? "merge" : "close"
recipients = build_recipients(target, project, current_user, action: action)
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 23b6668e0d1..f578f8dbea2 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -1,7 +1,7 @@
module Projects
class AutocompleteService < BaseService
def issues
- @project.issues.visible_to_user(current_user).opened.select([:iid, :title])
+ IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
def milestones
@@ -9,11 +9,34 @@ module Projects
end
def merge_requests
- @project.merge_requests.opened.select([:iid, :title])
+ MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
def labels
@project.labels.select([:title, :color])
end
+
+ def commands(noteable, type)
+ noteable ||=
+ case type
+ when 'Issue'
+ @project.issues.build
+ when 'MergeRequest'
+ @project.merge_requests.build
+ end
+
+ return [] unless noteable && noteable.is_a?(Issuable)
+
+ opts = {
+ project: project,
+ issuable: noteable,
+ current_user: current_user
+ }
+ SlashCommands::InterpretService.command_definitions.map do |definition|
+ next unless definition.available?(opts)
+
+ definition.to_h(opts)
+ end.compact
+ end
end
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 882606e38d0..8a53f65aec1 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -6,8 +6,12 @@ module Projects
DELETED_FLAG = '+deleted'
- def pending_delete!
- project.schedule_delete!(current_user.id, params)
+ def async_execute
+ project.transaction do
+ project.update_attribute(:pending_delete, true)
+ job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params)
+ Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}")
+ end
end
def execute
diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb
new file mode 100644
index 00000000000..3cf4264ce9b
--- /dev/null
+++ b/app/services/projects/enable_deploy_key_service.rb
@@ -0,0 +1,17 @@
+module Projects
+ class EnableDeployKeyService < BaseService
+ def execute
+ key = accessible_keys.find_by(id: params[:key_id] || params[:id])
+ return unless key
+
+ project.deploy_keys << key
+ key
+ end
+
+ private
+
+ def accessible_keys
+ current_user.accessible_deploy_keys
+ end
+ end
+end
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
index 752c11d7ae6..29b3981f49f 100644
--- a/app/services/projects/housekeeping_service.rb
+++ b/app/services/projects/housekeeping_service.rb
@@ -7,8 +7,6 @@
#
module Projects
class HousekeepingService < BaseService
- include Gitlab::ShellAdapter
-
LEASE_TIMEOUT = 3600
class LeaseTaken < StandardError
@@ -24,11 +22,7 @@ module Projects
def execute
raise LeaseTaken unless try_obtain_lease
- GitlabShellOneShotWorker.perform_async(:gc, @project.repository_storage_path, @project.path_with_namespace)
- ensure
- Gitlab::Metrics.measure(:reset_pushes_since_gc) do
- update_pushes_since_gc(0)
- end
+ execute_gitlab_shell_gc
end
def needed?
@@ -36,19 +30,27 @@ module Projects
end
def increment!
- Gitlab::Metrics.measure(:increment_pushes_since_gc) do
- update_pushes_since_gc(@project.pushes_since_gc + 1)
+ if Gitlab::ExclusiveLease.new("project_housekeeping:increment!:#{@project.id}", timeout: 60).try_obtain
+ Gitlab::Metrics.measure(:increment_pushes_since_gc) do
+ update_pushes_since_gc(@project.pushes_since_gc + 1)
+ end
end
end
private
- def update_pushes_since_gc(new_value)
- if Gitlab::ExclusiveLease.new("project_housekeeping:update_pushes_since_gc:#{project.id}", timeout: 60).try_obtain
- @project.update_column(:pushes_since_gc, new_value)
+ def execute_gitlab_shell_gc
+ GitGarbageCollectWorker.perform_async(@project.id)
+ ensure
+ Gitlab::Metrics.measure(:reset_pushes_since_gc) do
+ update_pushes_since_gc(0)
end
end
+ def update_pushes_since_gc(new_value)
+ @project.update_column(:pushes_since_gc, new_value)
+ end
+
def try_obtain_lease
Gitlab::Metrics.measure(:obtain_housekeeping_lease) do
lease = ::Gitlab::ExclusiveLease.new("project_housekeeping:#{@project.id}", timeout: LEASE_TIMEOUT)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 6afc048576d..06252c7b625 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -9,8 +9,8 @@ module Projects
private
def save_all
- if [version_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
- Gitlab::ImportExport::Saver.save(shared: @shared)
+ if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+ Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success
else
cleanup_and_notify
@@ -21,6 +21,10 @@ module Projects
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
+ def avatar_saver
+ Gitlab::ImportExport::AvatarSaver.new(project: project, shared: @shared)
+ end
+
def project_tree_saver
Gitlab::ImportExport::ProjectTreeSaver.new(project: project, shared: @shared)
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 163ebf26c84..cdad0426b02 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -43,7 +43,7 @@ module Projects
def import_repository
begin
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
- rescue Gitlab::Shell::Error => e
+ rescue => e
raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 02c4eee3d02..d38328403c1 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,40 +1,28 @@
module Projects
class ParticipantsService < BaseService
- def execute(noteable_type, noteable_id)
- @noteable_type = noteable_type
- @noteable_id = noteable_id
+ attr_reader :noteable
+
+ def execute(noteable)
+ @noteable = noteable
+
project_members = sorted(project.team.members)
- participants = target_owner + participants_in_target + all_members + groups + project_members
+ participants = noteable_owner + participants_in_noteable + all_members + groups + project_members
participants.uniq
end
- def target
- @target ||=
- case @noteable_type
- when "Issue"
- project.issues.find_by_iid(@noteable_id)
- when "MergeRequest"
- project.merge_requests.find_by_iid(@noteable_id)
- when "Commit"
- project.commit(@noteable_id)
- else
- nil
- end
- end
-
- def target_owner
- return [] unless target && target.author.present?
+ def noteable_owner
+ return [] unless noteable && noteable.author.present?
[{
- name: target.author.name,
- username: target.author.username
+ name: noteable.author.name,
+ username: noteable.author.username
}]
end
- def participants_in_target
- return [] unless target
+ def participants_in_noteable
+ return [] unless noteable
- users = target.participants(current_user)
+ users = noteable.participants(current_user)
sorted(users)
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index f06311511cc..921ca6748d3 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,7 +3,7 @@ module Projects
def execute
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
-
+
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)
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
new file mode 100644
index 00000000000..a84e335340d
--- /dev/null
+++ b/app/services/protected_branches/create_service.rb
@@ -0,0 +1,11 @@
+module ProtectedBranches
+ class CreateService < BaseService
+ attr_reader :protected_branch
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+
+ project.protected_branches.create(params)
+ end
+ end
+end
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
new file mode 100644
index 00000000000..89d8ba60134
--- /dev/null
+++ b/app/services/protected_branches/update_service.rb
@@ -0,0 +1,13 @@
+module ProtectedBranches
+ class UpdateService < BaseService
+ attr_reader :protected_branch
+
+ def execute(protected_branch)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
+
+ @protected_branch = protected_branch
+ @protected_branch.update(params)
+ @protected_branch
+ end
+ end
+end
diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb
new file mode 100644
index 00000000000..aa84d36a206
--- /dev/null
+++ b/app/services/repository_archive_clean_up_service.rb
@@ -0,0 +1,33 @@
+class RepositoryArchiveCleanUpService
+ LAST_MODIFIED_TIME_IN_MINUTES = 120
+
+ attr_reader :mmin, :path
+
+ def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES)
+ @mmin = mmin
+ @path = Gitlab.config.gitlab.repository_downloads_path
+ end
+
+ def execute
+ Gitlab::Metrics.measure(:repository_archive_clean_up) do
+ return unless File.directory?(path)
+
+ clean_up_old_archives
+ clean_up_empty_directories
+ end
+ end
+
+ private
+
+ def clean_up_old_archives
+ run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete))
+ end
+
+ def clean_up_empty_directories
+ run(%W(find #{path} -not -path #{path} -type d -empty -name \*.git -maxdepth 1 -delete))
+ end
+
+ def run(cmd)
+ Gitlab::Popen.popen(cmd)
+ end
+end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
new file mode 100644
index 00000000000..9ac1124abc1
--- /dev/null
+++ b/app/services/slash_commands/interpret_service.rb
@@ -0,0 +1,236 @@
+module SlashCommands
+ class InterpretService < BaseService
+ include Gitlab::SlashCommands::Dsl
+
+ attr_reader :issuable
+
+ # Takes a text and interprets the commands that are extracted from it.
+ # Returns the content without commands, and hash of changes to be applied to a record.
+ def execute(content, issuable)
+ @issuable = issuable
+ @updates = {}
+
+ opts = {
+ issuable: issuable,
+ current_user: current_user,
+ project: project
+ }
+
+ content, commands = extractor.extract_commands(content, opts)
+
+ commands.each do |name, arg|
+ definition = self.class.command_definitions_by_name[name.to_sym]
+ next unless definition
+
+ definition.execute(self, opts, arg)
+ end
+
+ [content, @updates]
+ end
+
+ private
+
+ def extractor
+ Gitlab::SlashCommands::Extractor.new(self.class.command_definitions)
+ end
+
+ desc do
+ "Close this #{issuable.to_ability_name.humanize(capitalize: false)}"
+ end
+ condition do
+ issuable.persisted? &&
+ issuable.open? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :close do
+ @updates[:state_event] = 'close'
+ end
+
+ desc do
+ "Reopen this #{issuable.to_ability_name.humanize(capitalize: false)}"
+ end
+ condition do
+ issuable.persisted? &&
+ issuable.closed? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :reopen do
+ @updates[:state_event] = 'reopen'
+ end
+
+ desc 'Change title'
+ params '<New title>'
+ condition do
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :title do |title_param|
+ @updates[:title] = title_param
+ end
+
+ desc 'Assign'
+ params '@user'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :assign do |assignee_param|
+ user = extract_references(assignee_param, :user).first
+ user ||= User.find_by(username: assignee_param)
+
+ @updates[:assignee_id] = user.id if user
+ end
+
+ desc 'Remove assignee'
+ condition do
+ issuable.persisted? &&
+ issuable.assignee_id? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :unassign do
+ @updates[:assignee_id] = nil
+ end
+
+ desc 'Set milestone'
+ params '%"milestone"'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+ project.milestones.active.any?
+ end
+ command :milestone do |milestone_param|
+ milestone = extract_references(milestone_param, :milestone).first
+ milestone ||= project.milestones.find_by(title: milestone_param.strip)
+
+ @updates[:milestone_id] = milestone.id if milestone
+ end
+
+ desc 'Remove milestone'
+ condition do
+ issuable.persisted? &&
+ issuable.milestone_id? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :remove_milestone do
+ @updates[:milestone_id] = nil
+ end
+
+ desc 'Add label(s)'
+ params '~label1 ~"label 2"'
+ condition do
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
+ project.labels.any?
+ end
+ command :label do |labels_param|
+ label_ids = find_label_ids(labels_param)
+
+ @updates[:add_label_ids] = label_ids unless label_ids.empty?
+ end
+
+ desc 'Remove all or specific label(s)'
+ params '~label1 ~"label 2"'
+ condition do
+ issuable.persisted? &&
+ issuable.labels.any? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :unlabel do |labels_param = nil|
+ if labels_param.present?
+ label_ids = find_label_ids(labels_param)
+
+ @updates[:remove_label_ids] = label_ids unless label_ids.empty?
+ else
+ @updates[:label_ids] = []
+ end
+ end
+
+ desc 'Replace all label(s)'
+ params '~label1 ~"label 2"'
+ condition do
+ issuable.persisted? &&
+ issuable.labels.any? &&
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
+ end
+ command :relabel do |labels_param|
+ label_ids = find_label_ids(labels_param)
+
+ @updates[:label_ids] = label_ids unless label_ids.empty?
+ end
+
+ desc 'Add a todo'
+ condition do
+ issuable.persisted? &&
+ !TodoService.new.todo_exist?(issuable, current_user)
+ end
+ command :todo do
+ @updates[:todo_event] = 'add'
+ end
+
+ desc 'Mark todo as done'
+ condition do
+ issuable.persisted? &&
+ TodoService.new.todo_exist?(issuable, current_user)
+ end
+ command :done do
+ @updates[:todo_event] = 'done'
+ end
+
+ desc 'Subscribe'
+ condition do
+ issuable.persisted? &&
+ !issuable.subscribed?(current_user)
+ end
+ command :subscribe do
+ @updates[:subscription_event] = 'subscribe'
+ end
+
+ desc 'Unsubscribe'
+ condition do
+ issuable.persisted? &&
+ issuable.subscribed?(current_user)
+ end
+ command :unsubscribe do
+ @updates[:subscription_event] = 'unsubscribe'
+ end
+
+ desc 'Set due date'
+ params '<in 2 days | this Friday | December 31st>'
+ condition do
+ issuable.respond_to?(:due_date) &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :due do |due_date_param|
+ due_date = Chronic.parse(due_date_param).try(:to_date)
+
+ @updates[:due_date] = due_date if due_date
+ end
+
+ desc 'Remove due date'
+ condition do
+ issuable.persisted? &&
+ issuable.respond_to?(:due_date) &&
+ issuable.due_date? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :remove_due_date do
+ @updates[:due_date] = nil
+ end
+
+ # This is a dummy command, so that it appears in the autocomplete commands
+ desc 'CC'
+ params '@user'
+ command :cc
+
+ def find_label_ids(labels_param)
+ label_ids_by_reference = extract_references(labels_param, :label).map(&:id)
+ labels_ids_by_name = @project.labels.where(name: labels_param.split).select(:id)
+
+ label_ids_by_reference | labels_ids_by_name
+ end
+
+ def extract_references(arg, type)
+ ext = Gitlab::ReferenceExtractor.new(project, current_user)
+ ext.analyze(arg, author: current_user)
+
+ ext.references(type)
+ end
+ end
+end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
new file mode 100644
index 00000000000..48903291799
--- /dev/null
+++ b/app/services/spam_service.rb
@@ -0,0 +1,78 @@
+class SpamService
+ attr_accessor :spammable, :request, :options
+
+ def initialize(spammable, request = nil)
+ @spammable = spammable
+ @request = request
+ @options = {}
+
+ if @request
+ @options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
+ @options[:user_agent] = @request.env['HTTP_USER_AGENT']
+ @options[:referrer] = @request.env['HTTP_REFERRER']
+ else
+ @options[:ip_address] = @spammable.ip_address
+ @options[:user_agent] = @spammable.user_agent
+ end
+ end
+
+ def check(api = false)
+ return false unless request && check_for_spam?
+
+ return false unless akismet.is_spam?
+
+ create_spam_log(api)
+ true
+ end
+
+ def mark_as_spam!
+ return false unless spammable.submittable_as_spam?
+
+ if akismet.submit_spam
+ spammable.user_agent_detail.update_attribute(:submitted, true)
+ else
+ false
+ end
+ end
+
+ private
+
+ def akismet
+ @akismet ||= AkismetService.new(
+ spammable_owner,
+ spammable.spammable_text,
+ options
+ )
+ end
+
+ def spammable_owner
+ @user ||= User.find(spammable_owner_id)
+ end
+
+ def spammable_owner_id
+ @owner_id ||=
+ if spammable.respond_to?(:author_id)
+ spammable.author_id
+ elsif spammable.respond_to?(:creator_id)
+ spammable.creator_id
+ end
+ end
+
+ def check_for_spam?
+ spammable.check_for_spam?
+ end
+
+ def create_spam_log(api)
+ SpamLog.create(
+ {
+ user_id: spammable_owner_id,
+ title: spammable.spam_title,
+ description: spammable.spam_description,
+ source_ip: options[:ip_address],
+ user_agent: options[:user_agent],
+ noteable_type: spammable.class.to_s,
+ via_api: api
+ }
+ )
+ end
+end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1ab3b5789bc..0c8446e7c3d 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -2,7 +2,9 @@
#
# Used for creating system notes (e.g., when a user references a merge request
# from an issue, an issue's assignee changes, an issue is closed, etc.)
-class SystemNoteService
+module SystemNoteService
+ extend self
+
# Called when commits are added to a Merge Request
#
# noteable - Noteable object
@@ -15,7 +17,7 @@ class SystemNoteService
# See new_commit_summary and existing_commit_summary.
#
# Returns the created Note object
- def self.add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil)
+ def add_commits(noteable, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = "#{total_count} commit".pluralize(total_count)
@@ -40,7 +42,7 @@ class SystemNoteService
# "Reassigned to @rspeicher"
#
# Returns the created Note object
- def self.change_assignee(noteable, project, author, assignee)
+ def change_assignee(noteable, project, author, assignee)
body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
@@ -63,7 +65,7 @@ class SystemNoteService
# "Removed ~5 label"
#
# Returns the created Note object
- def self.change_label(noteable, project, author, added_labels, removed_labels)
+ def change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
references = ->(label) { label.to_reference(format: :id) }
@@ -101,7 +103,7 @@ class SystemNoteService
# "Miletone changed to 7.11"
#
# Returns the created Note object
- def self.change_milestone(noteable, project, author, milestone)
+ def change_milestone(noteable, project, author, milestone)
body = 'Milestone '
body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
@@ -123,7 +125,7 @@ class SystemNoteService
# "Status changed to closed by bc17db76"
#
# Returns the created Note object
- def self.change_status(noteable, project, author, status, source)
+ def change_status(noteable, project, author, status, source)
body = "Status changed to #{status}"
body << " by #{source.gfm_reference(project)}" if source
@@ -131,31 +133,37 @@ class SystemNoteService
end
# Called when 'merge when build succeeds' is executed
- def self.merge_when_build_succeeds(noteable, project, author, last_commit)
+ 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"
create_note(noteable: noteable, project: project, author: author, note: body)
end
# Called when 'merge when build succeeds' is canceled
- def self.cancel_merge_when_build_succeeds(noteable, project, author)
+ def cancel_merge_when_build_succeeds(noteable, project, author)
body = 'Canceled the automatic merge'
create_note(noteable: noteable, project: project, author: author, note: body)
end
- def self.remove_merge_request_wip(noteable, project, author)
+ def remove_merge_request_wip(noteable, project, author)
body = 'Unmarked this merge request as a Work In Progress'
create_note(noteable: noteable, project: project, author: author, note: body)
end
- def self.add_merge_request_wip(noteable, project, author)
+ def add_merge_request_wip(noteable, project, author)
body = 'Marked this merge request 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"
+
+ create_note(noteable: merge_request, project: project, author: author, note: body)
+ end
+
# Called when the title of a Noteable is changed
#
# noteable - Noteable object that responds to `title`
@@ -168,7 +176,7 @@ class SystemNoteService
# "Title changed from **Old** to **New**"
#
# Returns the created Note object
- def self.change_title(noteable, project, author, old_title)
+ def change_title(noteable, project, author, old_title)
new_title = noteable.title.dup
old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_title, new_title).inline_diffs
@@ -191,7 +199,7 @@ class SystemNoteService
# "Made the issue confidential"
#
# Returns the created Note object
- def self.change_issue_confidentiality(issue, project, author)
+ def change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'Made the issue confidential' : 'Made the issue visible'
create_note(noteable: issue, project: project, author: author, note: body)
end
@@ -210,7 +218,7 @@ class SystemNoteService
# "Target branch changed from `Old` to `New`"
#
# Returns the created Note object
- def self.change_branch(noteable, project, author, branch_type, old_branch, new_branch)
+ def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
body = "#{branch_type} branch changed from `#{old_branch}` to `#{new_branch}`".capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -229,7 +237,7 @@ class SystemNoteService
# "Restored target branch `feature`"
#
# Returns the created Note object
- def self.change_branch_presence(noteable, project, author, branch_type, branch, presence)
+ def change_branch_presence(noteable, project, author, branch_type, branch, presence)
verb =
if presence == :add
'restored'
@@ -245,7 +253,7 @@ class SystemNoteService
# Example note text:
#
# "Started branch `201-issue-branch-button`"
- def self.new_issue_branch(issue, project, author, branch)
+ def new_issue_branch(issue, project, author, branch)
h = Gitlab::Routing.url_helpers
link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
@@ -261,16 +269,16 @@ class 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.
#
# Returns the created Note object
- def self.cross_reference(noteable, mentioner, author)
+ def cross_reference(noteable, mentioner, author)
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner.gfm_reference(noteable.project)
@@ -294,13 +302,13 @@ class SystemNoteService
end
end
- def self.cross_reference?(note_text)
+ def cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
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).
#
@@ -308,7 +316,7 @@ class SystemNoteService
# mentioner - Mentionable object
#
# Returns Boolean
- def self.cross_reference_disallowed?(noteable, mentioner)
+ def cross_reference_disallowed?(noteable, mentioner)
return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
@@ -328,7 +336,7 @@ class SystemNoteService
#
# Returns Boolean
- def self.cross_reference_exists?(noteable, mentioner)
+ def cross_reference_exists?(noteable, mentioner)
# Initial scope should be system notes of this noteable type
notes = Note.system.where(noteable_type: noteable.class)
@@ -342,9 +350,60 @@ class SystemNoteService
notes_for_mentioner(mentioner, noteable, notes).count > 0
end
+ # Build an Array of lines detailing each commit added in a merge request
+ #
+ # new_commits - Array of new Commit objects
+ #
+ # Returns an Array of Strings
+ def new_commit_summary(new_commits)
+ new_commits.collect do |commit|
+ "* #{commit.short_id} - #{escape_html(commit.title)}"
+ end
+ end
+
+ # Called when the status of a Task has changed
+ #
+ # noteable - Noteable object.
+ # project - Project owning noteable
+ # author - User performing the change
+ # new_task - TaskList::Item object.
+ #
+ # Example Note text:
+ #
+ # "Soandso 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}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when noteable has been moved to another project
+ #
+ # direction - symbol, :to or :from
+ # noteable - Noteable object
+ # noteable_ref - Referenced noteable
+ # author - User performing the move
+ #
+ # Example Note text:
+ #
+ # "Moved to some_namespace/project_new#11"
+ #
+ # Returns the created Note object
+ def noteable_moved(noteable, project, noteable_ref, author, direction:)
+ unless [:to, :from].include?(direction)
+ raise ArgumentError, "Invalid direction `#{direction}`"
+ end
+
+ cross_reference = noteable_ref.to_reference(project)
+ body = "Moved #{direction} #{cross_reference}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
private
- def self.notes_for_mentioner(mentioner, noteable, notes)
+ def notes_for_mentioner(mentioner, noteable, notes)
if mentioner.is_a?(Commit)
notes.where('note LIKE ?', "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}")
else
@@ -353,29 +412,18 @@ class SystemNoteService
end
end
- def self.create_note(args = {})
+ def create_note(args = {})
Note.create(args.merge(system: true))
end
- def self.cross_reference_note_prefix
- 'mentioned in '
+ def cross_reference_note_prefix
+ 'Mentioned in '
end
- def self.cross_reference_note_content(gfm_reference)
+ def cross_reference_note_content(gfm_reference)
"#{cross_reference_note_prefix}#{gfm_reference}"
end
- # Build an Array of lines detailing each commit added in a merge request
- #
- # new_commits - Array of new Commit objects
- #
- # Returns an Array of Strings
- def self.new_commit_summary(new_commits)
- new_commits.collect do |commit|
- "* #{commit.short_id} - #{escape_html(commit.title)}"
- end
- end
-
# Build a single line summarizing existing commits being added in a merge
# request
#
@@ -392,7 +440,7 @@ class SystemNoteService
# "* ea0f8418 - 1 commit from branch `feature`"
#
# Returns a newline-terminated String
- def self.existing_commit_summary(noteable, existing_commits, oldrev = nil)
+ def existing_commit_summary(noteable, existing_commits, oldrev = nil)
return '' if existing_commits.empty?
count = existing_commits.size
@@ -415,47 +463,7 @@ class SystemNoteService
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end
- # Called when the status of a Task has changed
- #
- # noteable - Noteable object.
- # project - Project owning noteable
- # author - User performing the change
- # new_task - TaskList::Item object.
- #
- # Example Note text:
- #
- # "Soandso marked the task Whatever as completed."
- #
- # Returns the created Note object
- def self.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}"
- create_note(noteable: noteable, project: project, author: author, note: body)
- end
-
- # Called when noteable has been moved to another project
- #
- # direction - symbol, :to or :from
- # noteable - Noteable object
- # noteable_ref - Referenced noteable
- # author - User performing the move
- #
- # Example Note text:
- #
- # "Moved to some_namespace/project_new#11"
- #
- # Returns the created Note object
- def self.noteable_moved(noteable, project, noteable_ref, author, direction:)
- unless [:to, :from].include?(direction)
- raise ArgumentError, "Invalid direction `#{direction}`"
- end
-
- cross_reference = noteable_ref.to_reference(project)
- body = "Moved #{direction} #{cross_reference}"
- create_note(noteable: noteable, project: project, author: author, note: body)
- end
-
- def self.escape_html(text)
+ def escape_html(text)
Rack::Utils.escape_html(text)
end
end
diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb
index e85e58751e7..280c81f7d2d 100644
--- a/app/services/test_hook_service.rb
+++ b/app/services/test_hook_service.rb
@@ -1,6 +1,6 @@
class TestHookService
def execute(hook, current_user)
- data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user)
+ data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user)
hook.execute(data, 'push_hooks')
end
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 6bb0a72d30e..2aab8c736d6 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -142,10 +142,16 @@ class TodoService
# When user marks some todos as done
def mark_todos_as_done(todos, current_user)
- todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all)
+ mark_todos_as_done_by_ids(todos.select(&:id), current_user)
+ end
+
+ def mark_todos_as_done_by_ids(ids, current_user)
+ todos = current_user.todos.where(id: ids)
- todos.update_all(state: :done)
+ # Only return those that are not really on that state
+ marked_todos = todos.where.not(state: :done).update_all(state: :done)
current_user.update_todos_count_cache
+ marked_todos
end
# When user marks an issue as todo
@@ -154,6 +160,10 @@ class TodoService
create_todos(current_user, attributes)
end
+ def todo_exist?(issuable, current_user)
+ TodosFinder.new(current_user).execute.exists?(target: issuable)
+ end
+
private
def create_todos(users, attributes)
@@ -194,7 +204,7 @@ class TodoService
end
def create_assignment_todo(issuable, author)
- if issuable.assignee && issuable.assignee != author
+ if issuable.assignee
attributes = attributes_for_todo(issuable.project, issuable, author, Todo::ASSIGNED)
create_todos(issuable.assignee, attributes)
end
@@ -239,7 +249,6 @@ class TodoService
def filter_mentioned_users(project, target, author)
mentioned_users = target.mentioned_users(author)
mentioned_users = reject_users_without_access(mentioned_users, project, target)
- mentioned_users.delete(author)
mentioned_users.uniq
end
diff --git a/app/services/user_agent_detail_service.rb b/app/services/user_agent_detail_service.rb
new file mode 100644
index 00000000000..a1ee3df5fe1
--- /dev/null
+++ b/app/services/user_agent_detail_service.rb
@@ -0,0 +1,13 @@
+class UserAgentDetailService
+ attr_accessor :spammable, :request
+
+ def initialize(spammable, request)
+ @spammable, @request = spammable, request
+ end
+
+ def create
+ return unless request
+
+ spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 1cd93263c9f..b6c52ddac7a 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
class ArtifactUploader < CarrierWave::Uploader::Base
storage :file
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index a65a896e41e..fb3b5dfecd0 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
class AttachmentUploader < CarrierWave::Uploader::Base
include UploaderHelper
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 6135c3ad96f..71ff14a3f20 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
class AvatarUploader < CarrierWave::Uploader::Base
include UploaderHelper
@@ -14,4 +12,8 @@ class AvatarUploader < CarrierWave::Uploader::Base
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
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 1af9e9b0edb..3ac6030c21c 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
include UploaderHelper
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
@@ -33,16 +32,15 @@ class FileUploader < CarrierWave::Uploader::Base
end
def to_h
- filename = image? ? self.file.basename : self.file.filename
+ filename = image_or_video? ? self.file.basename : self.file.filename
escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})"
- markdown.prepend("!") if image?
+ markdown.prepend("!") if image_or_video?
{
alt: filename,
url: self.secure_url,
- is_image: image?,
markdown: markdown
}
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 046a1d641a9..4f356dd663e 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,5 +1,3 @@
-# encoding: utf-8
-
class LfsObjectUploader < CarrierWave::Uploader::Base
storage :file
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
index 5ef440f3367..b10ad71d052 100644
--- a/app/uploaders/uploader_helper.rb
+++ b/app/uploaders/uploader_helper.rb
@@ -1,16 +1,37 @@
# Extra methods for uploader
module UploaderHelper
+ IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
+ # 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
+ # on IE >= 9.
+ # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
+ VIDEO_EXT = %w[mp4 m4v mov webm ogv]
+
def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
+ extension_match?(IMAGE_EXT)
+ end
+
+ def video?
+ extension_match?(VIDEO_EXT)
+ end
+
+ def image_or_video?
+ image? || video?
+ end
+
+ def extension_match?(extensions)
+ return false unless file
+
+ extension =
+ if file.respond_to?(:extension)
+ file.extension
+ else
+ # Not all CarrierWave storages respond to :extension
+ File.extname(file.path).delete('.')
+ end
+
+ extensions.include?(extension.downcase)
end
def file_storage?
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index dd2e7ebd030..56bf6194914 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -1,6 +1,8 @@
- reporter = abuse_report.reporter
- user = abuse_report.user
%tr
+ %th.visible-xs-block.visible-sm-block
+ %strong User
%td
- if user
= link_to user.name, user
@@ -9,6 +11,7 @@
- else
(removed)
%td
+ %strong.subheading.visible-xs-block.visible-sm-block Reported by
- if reporter
= link_to reporter.name, reporter
- else
@@ -16,16 +19,16 @@
.light.small
= time_ago_with_tooltip(abuse_report.created_at)
%td
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ %strong.subheading.visible-xs-block.visible-sm-block Message
+ .message
+ = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
- data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
-
- %td
+ data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-sm btn-block btn-remove js-remove-tr"
- if user && !user.blocked?
- = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-block"
- else
- .btn.btn-xs.disabled
+ .btn.btn-sm.disabled.btn-block
Already Blocked
- = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
+ = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-sm btn-block btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index bc4a9cedb2c..7bbc75db9ff 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -1,17 +1,20 @@
-- page_title "Abuse Reports"
+- page_title 'Abuse Reports'
%h3.page-title Abuse Reports
%hr
-- if @abuse_reports.present?
- .table-holder
- %table.table
- %thead
- %tr
- %th User
- %th Reported by
- %th Message
- %th Primary action
- %th
- = render @abuse_reports
- = paginate @abuse_reports
-- else
- %h4 There are no abuse reports
+.abuse-reports
+ - if @abuse_reports.present?
+ .table-holder
+ %table.table
+ %thead.hidden-sm.hidden-xs
+ %tr
+ %th User
+ %th Reported by
+ %th.wide Message
+ %th Action
+ = render @abuse_reports
+ - else
+ .no-reports
+ %span.pull-left
+ There are no abuse reports!
+ .pull-left
+ = emoji_icon 'tada'
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index dc083e50178..9175b3d3f96 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -13,7 +13,7 @@
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
- Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown', 'markdown'), target: '_blank'}.
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-group
= f.label :logo, class: 'control-label'
.col-sm-10
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 8de28528cda..d929364fc96 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -38,11 +38,11 @@
= 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")
+ = link_to "(?)", help_page_path("integration/github")
, Bitbucket
- = link_to "(?)", help_page_path("integration", "bitbucket")
+ = link_to "(?)", help_page_path("integration/bitbucket")
and GitLab.com
- = link_to "(?)", help_page_path("integration", "gitlab")
+ = link_to "(?)", help_page_path("integration/gitlab")
.form-group
%label.control-label.col-sm-2 Enabled Git access protocols
.col-sm-10
@@ -109,7 +109,7 @@
Newly registered users will by default be external
%fieldset
- %legend Sign-in Restrictions
+ %legend Sign-up Restrictions
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -123,6 +123,49 @@
= f.check_box :send_user_confirmation_email
Send confirmation email on sign-up
.form-group
+ = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+ .form-group
+ = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :domain_blacklist_enabled do
+ = f.check_box :domain_blacklist_enabled
+ Enable domain blacklist for sign ups
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .radio
+ = label_tag :blacklist_type_file do
+ = radio_button_tag :blacklist_type, :file
+ .option-title
+ Upload blacklist file
+ .radio
+ = label_tag :blacklist_type_raw do
+ = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?
+ .option-title
+ Enter blacklist manually
+ .form-group.blacklist-file
+ = f.label :domain_blacklist_file, 'Blacklist file', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf'
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
+ .form-group.blacklist-raw
+ = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8
+ .help-block Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+
+ .form-group
+ = f.label :after_sign_up_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
+
+ %fieldset
+ %legend Sign-in Restrictions
+ .form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :signin_enabled do
@@ -148,11 +191,6 @@
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
.help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
.form-group
- = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
- .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
- .form-group
= f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
@@ -168,11 +206,6 @@
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled
.form-group
- = f.label :after_sign_up_text, class: 'control-label col-sm-2'
- .col-sm-10
- = f.text_area :after_sign_up_text, class: 'form-control', rows: 4
- .help-block Markdown enabled
- .form-group
= f.label :help_page_text, class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :help_page_text, class: 'form-control', rows: 4
@@ -195,6 +228,9 @@
= f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :max_artifacts_size, class: 'form-control'
+ .help-block
+ Set the maximum file size each build's artifacts can have
+ = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size")
- if Gitlab.config.registry.enabled
%fieldset
@@ -330,7 +366,9 @@
.col-sm-10
= f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control'
.help-block
- You can manage the repository storage paths in your gitlab.yml configuration file
+ Manage repository storage paths. Learn more in the
+ = succeed "." do
+ = link_to "repository storages documentation", help_page_path("administration/repository_storages")
%fieldset
%legend Repository Checks
@@ -350,6 +388,25 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ %fieldset
+ %legend Koding
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 9d722bd7382..107fc25244a 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,18 +1,24 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
+ = nav_link(controller: :requests_profiles) do
+ = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+ %span
+ Requests Profiles
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 967151bc33b..f29d9c94441 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -1,30 +1,42 @@
- project = build.project
-%tr.build
+%tr.build.commit
%td.status
= ci_status_with_icon(build.status)
- %td.build-link
- - if can?(current_user, :read_build, build.project)
- = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %strong Build ##{build.id}
- - else
- %strong Build ##{build.id}
+ %td
+ .branch-commit
+ - if can?(current_user, :read_build, build.project)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %span.build-link ##{build.id}
+ - else
+ %span.build-link ##{build.id}
- - if build.stuck?
- %i.fa.fa-warning.text-warning
+ - if build.ref
+ .icon-container
+ = build.tag? ? icon('tag') : icon('code-fork')
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
+ - else
+ .light none
+ .icon-container
+ = custom_icon("icon_commit")
- %td
- - if project
- = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id"
+ - if build.stuck?
+ %i.fa.fa-warning.text-warning
- %td
- = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+ .label-container
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
%td
- - if build.ref
- = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- - else
- .light none
+ - if project
+ = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project)
%td
- if build.try(:runner)
@@ -36,22 +48,15 @@
#{build.stage} / #{build.name}
%td
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.try(:trigger_request)
- %span.label.label-info triggered
- - if build.try(:allow_failure)
- %span.label.label-danger allowed to fail
-
- %td.duration
- if build.duration
- #{duration_in_words(build.finished_at, build.started_at)}
+ %p.duration
+ = custom_icon("icon_timer")
+ = duration_in_numbers(build.duration)
- %td.timestamp
- if build.finished_at
- %span #{time_ago_with_tooltip(build.finished_at)}
+ %p.finished-at
+ = icon("calendar")
+ %span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 1e60205f91a..3d77634d8fa 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -10,15 +10,20 @@
All
%span.badge.js-totalbuilds-count= @all_builds.count(:id)
+ %li{class: ('active' if @scope == 'pending')}
+ = link_to admin_builds_path(scope: :pending) do
+ Pending
+ %span.badge= number_with_delimiter(@all_builds.pending.count(:id))
+
%li{class: ('active' if @scope == 'running')}
= link_to admin_builds_path(scope: :running) do
Running
- %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
+ %span.badge= number_with_delimiter(@all_builds.running.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to admin_builds_path(scope: :finished) do
Finished
- %span.badge.js-running-count= number_with_delimiter(@all_builds.finished.count(:id))
+ %span.badge= number_with_delimiter(@all_builds.finished.count(:id))
.nav-controls
- if @all_builds.running_or_pending.any?
@@ -27,7 +32,7 @@
.row-content-block.second-block
#{(@scope || 'all').capitalize} builds
- %ul.content-list
+ %ul.content-list.builds-content-list
- if @builds.blank?
%li
.nothing-here-block No builds to show
@@ -37,15 +42,11 @@
%thead
%tr
%th Status
- %th Build ID
- %th Project
%th Commit
- %th Ref
+ %th Project
%th Runner
%th Name
- %th Tags
- %th Duration
- %th Finished at
+ %th
%th
- @builds.each do |build|
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b74da64f82e..c91ab4cb946 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,26 +1,28 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- %span
- Builds
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index a2ac407c159..e6687f43816 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -80,6 +80,10 @@
%span.pull-right
= Gitlab::Shell.new.version
%p
+ GitLab Workhorse
+ %span.pull-right
+ = Gitlab::Workhorse.version
+ %p
GitLab API
%span.pull-right
= API::API::version
@@ -108,7 +112,7 @@
%h4 Projects
.data
= link_to admin_namespaces_projects_path do
- %h1= number_with_delimiter(Project.count)
+ %h1= number_with_delimiter(Project.cached_count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
index 15aa059c93d..5c410a695bf 100644
--- a/app/views/admin/deploy_keys/new.html.haml
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
- = link_to "here", help_page_path("ssh", "README")
+ = link_to "here", help_page_path("ssh/README")
= f.text_area :key, class: "form-control thin_area", rows: 5
.form-actions
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 0cc405401cf..5f7fdfdb011 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -9,6 +9,10 @@
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ = render 'shared/allow_request_access', form: f
+
- if @group.new_record?
.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 59fd6c3fea0..77a11e49e20 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -1,20 +1,26 @@
-- css_class = '' unless local_assigns[:css_class]
+- css_class = 'no-description' if group.description.blank?
-%li.group-row.group-admin{ class: css_class }
- .group-avatar
- = image_tag group_icon(group), class: 'avatar hidden-xs'
- .group-details
- .title
- = link_to [:admin, group], class: 'group-name' do
- = group.name
- .group-stats
- %span>= pluralize(number_with_delimiter(group.projects.count), 'project')
- ,
- %span= pluralize(number_with_delimiter(group.users.count), 'member')
-
- - if group.description.present?
- .description
- = markdown(group.description, pipeline: :description)
- .group-controls.hidden-xs
+%li.group-row{ class: css_class }
+ .controls
= link_to 'Edit', edit_admin_group_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
+ = icon('bookmark')
+ = number_with_delimiter(group.projects.count)
+
+ %span
+ = icon('users')
+ = number_with_delimiter(group.users.count)
+
+ %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+ = visibility_level_icon(group.visibility_level, fw: false)
+
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ .title
+ = link_to [:admin, group], class: 'group-name' do
+ = group.name
+
+ - if group.description.present?
+ .description
+ = markdown(group.description, pipeline: :description)
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 522153b37e3..bb374694400 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -79,7 +79,7 @@
.panel-body.form-holder
%p.light
Read more about project permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
+ %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
%div
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index 7b388cf7862..c217490963f 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -3,7 +3,7 @@
System hooks
%p.light
- #{link_to "System hooks ", help_page_path("system_hooks", "system_hooks"), class: "vlink"} can be
+ #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
used for binding events when GitLab creates a User or Project.
%hr
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 448aa953548..602cfa9b6fc 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -28,6 +28,3 @@
.form-actions
= f.submit 'Save', class: 'btn btn-save js-save-button'
= link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
-
-:javascript
- new Labels();
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 7fbce25b2c4..1e755785d90 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -66,7 +66,7 @@
%ul.projects-list.content-list
- @projects.each_with_index do |project|
%li.project-row
- .controls.pull-right
+ .controls
- if project.archived
%span.label.label-warning archived
%span.label.label-gray
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 2c5aba71699..b2c607361b3 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -132,7 +132,7 @@
- else
passed.
- = link_to icon('question-circle'), help_page_path('administration', 'repository_checks')
+ = link_to icon('question-circle'), help_page_path('administration/repository_checks')
.form-group
= f.submit 'Trigger repository check', class: 'btn btn-primary'
diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml
new file mode 100644
index 00000000000..ae918086a57
--- /dev/null
+++ b/app/views/admin/requests_profiles/index.html.haml
@@ -0,0 +1,26 @@
+- @no_container = true
+- page_title 'Requests Profiles'
+= render 'admin/background_jobs/head'
+
+%div{ class: container_class }
+ %h3.page-title
+ = page_title
+
+ .bs-callout.clearfix
+ Pass the header
+ %code X-Profile-Token: #{@profile_token}
+ to profile the request
+
+ - if @profiles.present?
+ .prepend-top-default
+ - @profiles.each do |path, profiles|
+ .panel.panel-default.panel-small
+ .panel-heading
+ %code= path
+ %ul.content-list
+ - profiles.each do |profile|
+ %li
+ = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true}
+ - else
+ %p
+ No profiles found
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 8aea67f4497..4ce4eab8753 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,6 +24,11 @@
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
%td
+ - if spam_log.submitted_as_ham?
+ .btn.btn-xs.disabled
+ Submitted as ham
+ - else
+ = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
- if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
- else
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6956e5ab795..bfc6142067a 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -9,12 +9,20 @@
.light-well
%h4 CPU
.data
- %h1= "#{@cpus} cores"
+ - if @cpus
+ %h1= "#{@cpus.length} cores"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect CPU info
.col-sm-4
.light-well
%h4 Memory
.data
- %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}"
+ - if @memory
+ %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect memory info
.col-sm-4
.light-well
%h4 Disks
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index d3519f616f6..4bf1c9cde3c 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -14,7 +14,7 @@
%span It's you!
.user-email
= mail_to user.email, user.email
- .controls.pull-right
+ .controls
= link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn'
- unless user == current_user
.dropdown.inline
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index d37489bebea..76c9ed0ee8b 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -140,12 +140,10 @@
.panel-heading
This user is blocked
.panel-body
- %p Blocking user has the following effects:
+ %p A blocked user cannot:
%ul
- %li User will not be able to login
- %li User will not be able to access git repositories
- %li Personal projects will be left
- %li Owned groups will be left
+ %li Log in
+ %li Access Git repositories
%br
= link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- else
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index d54c7cad7be..fdea834ff45 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,53 +1,46 @@
- publicish_project_count = ProjectsFinder.new.execute(current_user).count
-%h3.page-title Welcome to GitLab!
-%p.light Self hosted Git management application.
-%hr
-%div
- .dashboard-intro-icon
- %i.fa.fa-bookmark-o
- .dashboard-intro-text
- %p.slead
- You don't have access to any projects right now.
- %br
- - if current_user.can_create_project?
- You can create up to
- %strong= pluralize(number_with_delimiter(current_user.projects_limit), "project") + "."
- - else
- If you are added to a project, it will be displayed here.
-
+.blank-state.blank-state-welcome
+ %h2.blank-state-welcome-title
+ Welcome to GitLab
+ %p.blank-state-text
+ Code, test, and deploy together
+.blank-state
+ .blank-state-icon
+ = custom_icon("project", size: 50)
+ %h3.blank-state-title
+ You don't have access to any projects right now
+ %p.blank-state-text
- if current_user.can_create_project?
- .link_holder
- = link_to new_project_path, class: "btn btn-new" do
- = icon('plus')
- New Project
+ You can create up to
+ %strong= number_with_delimiter(current_user.projects_limit)
+ = succeed "." do
+ = "project".pluralize(current_user.projects_limit)
+ - else
+ If you are added to a project, it will be displayed here.
+ - if current_user.can_create_project?
+ = link_to new_project_path, class: "btn btn-new" do
+ New project
- if current_user.can_create_group?
- %hr
- %div
- .dashboard-intro-icon
- %i.fa.fa-users
- .dashboard-intro-text
- %p.slead
- You can create a group for several dependent projects.
- %br
- Groups are the best way to manage projects and members.
- .link_holder
- = link_to new_group_path, class: "btn btn-new" do
- %i.fa.fa-plus
- New 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
- %hr
- %div
- .dashboard-intro-icon
- %i.fa.fa-globe
- .dashboard-intro-text
- %p.slead
- There are
- %strong= number_with_delimiter(publicish_project_count)
- public projects on this server.
- %br
- Public projects are an easy way to allow everyone to have read-only access.
- .link_holder
- = link_to trending_explore_projects_path, class: "btn btn-new" do
- Browse public projects
+ .blank-state
+ .blank-state-icon
+ = icon("globe")
+ %h3.blank-state-title
+ There are
+ = number_with_delimiter(publicish_project_count)
+ public projects on this server.
+ %p.blank-state-text
+ Public projects are an easy way to allow everyone to have read-only access.
+ = link_to trending_explore_projects_path, class: "btn btn-new" do
+ Browse projects
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 4565e752c1f..4f36a4a1c73 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -5,7 +5,8 @@
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-= render 'dashboard/projects_head'
+- if @projects.any? || params[:filter_projects]
+ = render 'dashboard/projects_head'
- if @last_push
= render "events/event_last_push", event: @last_push
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 98f302d2f93..b40395c74de 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,6 +1,7 @@
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
+ = author_avatar(todo, size: 40)
+
.todo-item.todo-block
- = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
.todo-title.title
- unless todo.build_failed?
= todo_target_state_pill(todo)
@@ -19,13 +20,13 @@
&middot; #{time_ago_with_tooltip(todo.created_at)}
- - if todo.pending?
- .todo-actions.pull-right
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
- Done
- = icon('spinner spin')
-
.todo-body
.todo-note
.md
= event_note(todo.body, project: todo.project)
+
+ - if todo.pending?
+ .todo-actions
+ = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading done-todo' do
+ Done
+ = icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index fc42e5dcc66..d320d3bcc1e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -9,14 +9,14 @@
%span
To do
%span.badge
- = todos_pending_count
+ = number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
%li{class: "todos-done #{todo_done_active}"}
= link_to todos_filter_path(state: 'done') do
%span
Done
%span.badge
- = todos_done_count
+ = number_with_delimiter(todos_done_count)
.nav-controls
- if @todos.any?(&:pending?)
@@ -43,6 +43,25 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Action'})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+
+
.prepend-top-default
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 8e81671b7e7..b7d3acac2b1 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -1,4 +1,4 @@
-= form_tag(user_omniauth_authorize_path("crowd"), id: 'new_crowd_user' ) do
+= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable?
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index a373f61bd3c..4debd3d608f 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,3 +1,7 @@
+- if inject_u2f_api?
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('u2f.js')
+
%div
.login-box
.login-heading
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index de18bc2d844..2e7da2747d0 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -5,4 +5,4 @@
- providers.each do |provider|
%span.light
- has_icon = provider_has_icon?(provider)
- = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
+ = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
new file mode 100644
index 00000000000..1411daeb4a6
--- /dev/null
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -0,0 +1,6 @@
+- expanded = local_assigns.fetch(:expanded, true)
+%tr.notes_holder{class: ('hide' unless expanded)}
+ %td.notes_line{ colspan: 2 }
+ %td.notes_content
+ .content
+ = render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
new file mode 100644
index 00000000000..3a95a652810
--- /dev/null
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -0,0 +1,17 @@
+- diff_file = discussion.diff_file
+- blob = discussion.blob
+
+.diff-file.file-holder
+ .file-title
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: discussion.project, url: discussion_diff_path(discussion)
+
+ .diff-content.code.js-syntax-highlight
+ %table
+ - discussions = { discussion.original_line_code => discussion }
+ = render partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: diff_file,
+ discussions: discussions,
+ discussion_expanded: true,
+ plain: true }
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
new file mode 100644
index 00000000000..077e8e64e5f
--- /dev/null
+++ b/app/views/discussions/_discussion.html.haml
@@ -0,0 +1,48 @@
+- 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
+ .discussion-actions
+ = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
+ - if expanded
+ = icon("chevron-up")
+ - else
+ = icon("chevron-down")
+
+ Toggle discussion
+
+ = link_to_member(@project, discussion.author, avatar: false)
+
+ .inline.discussion-headline-light
+ = discussion.author.to_reference
+ started a discussion on
+
+ - if discussion.for_commit?
+ - commit = discussion.noteable
+ - if commit
+ commit
+ = link_to commit.short_id, namespace_project_commit_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code), class: 'monospace'
+ - else
+ a deleted commit
+ - else
+ - if discussion.active?
+ = link_to diffs_namespace_project_merge_request_path(discussion.project.namespace, discussion.project, discussion.noteable, anchor: discussion.line_code) do
+ the diff
+ - else
+ 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) }
+ - if discussion.diff_discussion? && discussion.diff_file
+ = render "discussions/diff_with_notes", discussion: discussion
+ - else
+ .panel.panel-default
+ = render "discussions/notes", discussion: discussion
diff --git a/app/views/discussions/_headline.html.haml b/app/views/discussions/_headline.html.haml
new file mode 100644
index 00000000000..c1dabeed387
--- /dev/null
+++ b/app/views/discussions/_headline.html.haml
@@ -0,0 +1,14 @@
+- if discussion.resolved?
+ .discussion-headline-light.js-discussion-headline
+ Resolved
+ - if discussion.resolved_by
+ by
+ = link_to_member(@project, discussion.resolved_by, avatar: false)
+ = time_ago_with_tooltip(discussion.resolved_at, placement: "bottom")
+- elsif discussion.last_updated_at != discussion.created_at
+ .discussion-headline-light.js-discussion-headline
+ Last updated
+ - if discussion.last_updated_by
+ by
+ = link_to_member(@project, discussion.last_updated_by, avatar: false)
+ = time_ago_with_tooltip(discussion.last_updated_at, placement: "bottom")
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
new file mode 100644
index 00000000000..69bd416c4de
--- /dev/null
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -0,0 +1,9 @@
+- discussion = local_assigns.fetch(:discussion, nil)
+- if current_user
+ %jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
+ .btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
+ %button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
+ title: "Jump to next unresolved discussion",
+ "aria-label" => "Jump to next unresolved discussion",
+ data: { container: "body" } }
+ = custom_icon("next_discussion")
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
new file mode 100644
index 00000000000..fbe470bed2c
--- /dev/null
+++ b/app/views/discussions/_notes.html.haml
@@ -0,0 +1,15 @@
+%ul.notes{ data: { discussion_id: discussion.id } }
+ = render partial: "projects/notes/note", collection: discussion.notes, as: :note
+
+- if current_user
+ .discussion-reply-holder
+ - if discussion.diff_discussion?
+ - line_type = local_assigns.fetch(:line_type, nil)
+
+ .btn-group-justified.discussion-with-resolve-btn{ role: "group" }
+ .btn-group{ role: "group" }
+ = link_to_reply_discussion(discussion, line_type)
+ = render "discussions/resolve_all", discussion: discussion
+ = render "discussions/jump_to_next", discussion: discussion
+ - else
+ = link_to_reply_discussion(discussion)
diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml
new file mode 100644
index 00000000000..f1072ce0feb
--- /dev/null
+++ b/app/views/discussions/_parallel_diff_discussion.html.haml
@@ -0,0 +1,21 @@
+- expanded = discussion_left.try(:expanded?) || discussion_right.try(:expanded?)
+%tr.notes_holder{class: ('hide' unless expanded)}
+ - if discussion_left
+ %td.notes_line.old
+ %td.notes_content.parallel.old
+ .content{class: ('hide' unless discussion_left.expanded?)}
+ = render "discussions/notes", discussion: discussion_left, line_type: 'old'
+ - else
+ %td.notes_line.old= ""
+ %td.notes_content.parallel.old
+ .content
+
+ - if discussion_right
+ %td.notes_line.new
+ %td.notes_content.parallel.new
+ .content{class: ('hide' unless discussion_right.expanded?)}
+ = render "discussions/notes", discussion: discussion_right, line_type: 'new'
+ - else
+ %td.notes_line.new= ""
+ %td.notes_content.parallel.new
+ .content
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
new file mode 100644
index 00000000000..7a8767ddba0
--- /dev/null
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -0,0 +1,11 @@
+- if discussion.for_merge_request?
+ %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
+ ":project-path" => "'#{discussion.project.path}'",
+ ":discussion-id" => "'#{discussion.id}'",
+ ":merge-request-id" => discussion.noteable.iid,
+ ":can-resolve" => discussion.can_resolve?(current_user),
+ "inline-template" => true }
+ .btn-group{ role: "group", "v-if" => "showButton" }
+ %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" }
+ = icon("spinner spin", "v-show" => "loading")
+ {{ buttonText }}
diff --git a/app/views/emojis/index.html.haml b/app/views/emojis/index.html.haml
index 8b38b4c2bd4..790d90ad3ee 100644
--- a/app/views/emojis/index.html.haml
+++ b/app/views/emojis/index.html.haml
@@ -1,5 +1,5 @@
.emoji-menu
- = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Seach emojis"
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji"
.emoji-menu-content
- Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index 012e9857642..c034bbe430e 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -3,4 +3,4 @@
%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("permissions", "permissions"), class: "vlink"}
+%p Read more about project permissions #{link_to "here", help_page_path("user/permissions"), class: "vlink"}
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index e4629bae0e6..5c318cd3b8b 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -4,11 +4,7 @@
#{time_ago_with_tooltip(event.created_at)}
= cache [event, current_application_settings, "v2.2"] do
- - if event.author
- = link_to user_path(event.author) do
- = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
- - else
- = image_tag avatar_icon(event.author_email, 40), class: "avatar s40", alt:''
+ = author_avatar(event, size: 40)
- if event.created_project?
= render "events/event/created_project", event: event
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
new file mode 100644
index 00000000000..8f7da7d8c4f
--- /dev/null
+++ b/app/views/events/_event_scope.html.haml
@@ -0,0 +1,7 @@
+%span.event-scope
+ = event_preposition(event)
+ - if event.project
+ = link_to_project event.project
+ - else
+ = event.project_name
+
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 2e2403347c1..bba6e0d2c20 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,6 +1,6 @@
.event-title
%span.author_name= link_to_author event
- %span.event_label{class: event.action_name}
+ %span{class: event.action_name}
- if event.target
= event.action_name
%strong
@@ -10,12 +10,7 @@
- else
= event_action_name(event)
- = event_preposition(event)
-
- - if event.project
- = link_to_project event.project
- - else
- = event.project_name
+ = render "events/event_scope", event: event
- if event.target.respond_to?(:title)
.event-body
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 5a2a469ba62..aba64dd17d0 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,6 +1,6 @@
.event-title
%span.author_name= link_to_author event
- %span.event_label{class: event.action_name}
+ %span{class: event.action_name}
= event_action_name(event)
- if event.project
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 830fec0b4ab..f08c96df309 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,14 +1,9 @@
.event-title
%span.author_name= link_to_author event
- %span.event_label
- = event.action_name
- = event_note_title_html(event)
- at
+ = event.action_name
+ = event_note_title_html(event)
- - if event.project
- = link_to_project event.project
- - else
- = event.project_name
+ = render "events/event_scope", event: event
.event-body
.event-note
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index ea54ef226ec..44fff49d99c 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -2,14 +2,14 @@
.event-title
%span.author_name= link_to_author event
- %span.event_label.pushed #{event.action_name} #{event.ref_type}
+ %span.pushed #{event.action_name} #{event.ref_type}
- if event.rm_ref?
%strong= event.ref_name
- else
%strong
= link_to event.ref_name, namespace_project_commits_path(project.namespace, project, event.ref_name), title: h(event.target_title)
- at
- = link_to_project project
+
+ = render "events/event_scope", event: event
- if event.push_with_commits?
.event-body
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 57f6e7e0612..b8248a80a27 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -24,7 +24,7 @@
- else
= sort_title_recently_created
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
= sort_title_recently_created
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 92cd4c553d0..decb89b2fd6 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -22,6 +22,10 @@
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group
+ .col-sm-offset-2.col-sm-10
+ = render 'shared/allow_request_access', form: f
+
+ .form-group
%hr
= f.label :share_with_group_lock, class: 'control-label' do
Share with group lock
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index e7ab4f2409b..2fb3190ab11 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -12,7 +12,16 @@
= select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
+ %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this group and all of its projects.
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 90f362c052b..f789796e942 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -17,7 +17,7 @@
.panel-heading
%strong #{@group.name}
group members
- %span.badge= @members.size
+ %span.badge= @members.total_count
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e..742f9d7a433 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 121a7de3ad7..a8fdbd8c426 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -6,7 +6,6 @@
.nav-controls
- if can?(current_user, :admin_milestones, @group)
= link_to new_group_milestone_path(@group), class: "btn btn-new" do
- = icon('plus')
New Milestone
.row-content-block
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index c2f2d9912f7..33fee334d93 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -7,7 +7,6 @@
- if can? current_user, :admin_group, @group
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
- = icon('plus')
New Project
%ul.well-list
- @projects.each do |project|
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a83eb7e88bb..53ed4fa991d 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,8 +6,7 @@
.cover-block.groups-cover-block
%div{ class: container_class }
- = link_to group_icon(@group), target: '_blank' do
- = image_tag group_icon(@group), class: "avatar group-avatar s70"
+ = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile"
.group-info
.cover-title
%h1
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 8cc0b59edeb..ce4536ebdc6 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -20,6 +20,10 @@
%td Focus Search
%tr
%td.shortcut
+ .key f
+ %td Focus Filter
+ %tr
+ %td.shortcut
.key ?
%td Show/hide this dialog
%tr
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index 0398afb4c1d..be257b51b9e 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
-- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize)
+- page_title @path.split("/").reverse.map(&:humanize)
.documentation.wiki
= markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d676bc28c89..d16bd61b779 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -189,7 +189,7 @@
%li
%a Sort by date
- = link_to 'New issue', '#', class: 'btn btn-new'
+ = link_to 'New issue', '#', class: 'btn btn-new btn-inverted'
.lead
Only nav links without button and search
@@ -549,4 +549,4 @@
%li wiki page
%li help page
- You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown", "markdown")}.
+ You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}.
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 6e993e58f0d..15dd98077c8 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -74,6 +74,4 @@
= link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true"
again.
-
-:javascript
- new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}");
+.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index d3d3c595c17..c8a6fa1aa9e 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -56,5 +56,4 @@
Import
= icon("spinner spin", class: "loading-icon")
-:javascript
- new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}");
+.js-importer-status{ data: { jobs_import_path: "#{jobs_import_fogbugz_path}", import_path: "#{import_fogbugz_path}" } }
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 435ed7bd4cb..4c6af0b7908 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -38,6 +38,6 @@
As an administrator you may like to configure
- else
Consider asking your GitLab administrator to configure
- = link_to 'GitHub integration', help_page_path("integration", "github")
+ = link_to 'GitHub integration', help_page_path("integration/github")
which will allow login via GitHub and allow importing projects without
generating a Personal Access Token.
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 7486b1423e2..54ff1d27c67 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -4,10 +4,6 @@
%i.fa.fa-github
Import projects from GitHub
-%p
- %i.fa.fa-warning
- To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
-
%p.light
Select projects you want to import.
%hr
@@ -55,5 +51,4 @@
Import
= icon("spinner spin", class: "loading-icon")
-:javascript
- new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}");
+.js-importer-status{ data: { jobs_import_path: "#{jobs_import_github_path}", import_path: "#{import_github_path}" } }
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index aedb8468eca..fcfc6fd37f4 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -51,5 +51,4 @@
Import
= icon("spinner spin", class: "loading-icon")
-:javascript
- new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}");
+.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitlab_path}", import_path: "#{import_gitlab_path}" } }
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
deleted file mode 100644
index 267eee4f262..00000000000
--- a/app/views/import/gitorious/status.html.haml
+++ /dev/null
@@ -1,55 +0,0 @@
-- page_title "Gitorious import"
-- header_title "Projects", root_path
-%h3.page-title
- %i.icon-gitorious.icon-gitorious-big
- Import projects from Gitorious.org
-
-%p.light
- Select projects you want to import.
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th From Gitorious.org
- %th To GitLab
- %th Status
- %tbody
- - @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://gitorious.org/#{project.import_source}", target: "_blank"
- %td
- = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
- %td.job-status
- - if project.import_status == 'finished'
- %span
- %i.fa.fa-check
- done
- - elsif project.import_status == 'started'
- %i.fa.fa-spinner.fa-spin
- started
- - else
- = project.human_import_status_name
-
- - @repos.each do |repo|
- %tr{id: "repo_#{repo.id}"}
- %td
- = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
- %td.import-target
- = repo.full_name
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- Import
- = icon("spinner spin", class: "loading-icon")
-
-:javascript
- new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}");
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5ada6b174eb..e79f122940a 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -77,5 +77,4 @@
= link_to "import flow", new_import_google_code_path
again.
-:javascript
- new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}");
+.js-importer-status{ data: { jobs_import_path: "#{jobs_import_google_code_path}", import_path: "#{import_google_code_path}" } }
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
new file mode 100644
index 00000000000..65887aacbaf
--- /dev/null
+++ b/app/views/koding/index.html.haml
@@ -0,0 +1,6 @@
+.row-content-block.second-block.center
+ %p
+ = icon('circle', class: 'cgreen')
+ Integration is active for
+ = link_to koding_project_url, target: '_blank' do
+ #{current_application_settings.koding_url}
diff --git a/app/views/layouts/_collapse_button.html.haml b/app/views/layouts/_collapse_button.html.haml
deleted file mode 100644
index 8c140a5943e..00000000000
--- a/app/views/layouts/_collapse_button.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-= link_to '#', class: 'nav-header-btn text-center toggle-nav-collapse', title: "Open/Close" do
- %span.sr-only Toggle navigation
- = icon('bars')
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index cc8ea066cb9..3612f1ce5c6 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,4 +1,4 @@
-.flash-container
+.flash-container.flash-container-page
- if alert
.flash-alert
= alert
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 12e7ed0e792..67ff4b272b9 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,7 +1,7 @@
- project = @target_project || @project
+- noteable_type = @noteable.class if @noteable.present?
-- if @noteable
- :javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
- GitLab.GfmAutoComplete.cachedData = undefined;
- GitLab.GfmAutoComplete.setup();
+: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();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8596bbfdef6..bf50633af24 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,6 +1,13 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
- = render partial: 'layouts/collapse_button'
+ .sidebar-action-buttons
+ = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
+ %span.sr-only Toggle navigation
+ = icon('bars')
+ = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
+ %span.sr-only Toggle navigation pinning
+ = icon('fw thumb-tack')
+
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
@@ -8,9 +15,6 @@
- else
= render 'layouts/nav/explore'
- = link_to '#', class: "nav-header-btn text-center pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
- %span.sr-only Toggle navigation pinning
- = icon('thumb-tack')
- if defined?(nav) && nav
.layout-nav
.container-fluid
@@ -19,7 +23,6 @@
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
- %div{ class: (container_class unless @no_container) }
+ %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content
- .clearfix
- = yield
+ = yield
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 245b9c3b4d4..f7580f00159 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -44,7 +44,7 @@
name: "#{j(@project.name)}"
};
- - if @group and @group.path
+ - if @group && @group.persisted? && @group.path
:javascript
gl.groupOptions = gl.groupOptions || {};
gl.groupOptions["#{j(@group.path)}"] = {
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 33cedaaf2ee..15a94ac23c5 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,5 +1,5 @@
!!! 5
-%html{ lang: "en"}
+%html{ lang: "en", class: "#{page_class}" }
= render "layouts/head"
%body{class: "#{user_application_theme}", data: {page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}"}}
= Gon::Base.render_data
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 11cee421a99..94c53882623 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,7 +1,7 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
%div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
- %button.side-nav-toggle{type: 'button'}
+ %button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
= icon('bars')
%button.navbar-toggle{type: 'button'}
@@ -13,25 +13,25 @@
%li.hidden-sm.hidden-xs
= render 'layouts/search' unless current_controller?(:search)
%li.visible-sm.visible-xs
- = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to search_path, title: 'Search', aria: { label: "Search" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
- if current_user
- if session[:impersonator_id]
%li.impersonation
- = link_to admin_impersonation_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = link_to admin_impersonation_path, method: :delete, title: "Stop Impersonation", aria: { label: 'Stop Impersonation' }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret fw')
- if current_user.is_admin?
%li
- = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
%li
- = link_to dashboard_todos_path, title: 'Todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count
- if current_user.can_create_project?
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
@@ -45,12 +45,12 @@
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li
- = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
+ = link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
- = link_to "Profile Settings", profile_path
+ = link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
%li.divider
%li
- = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", title: 'Sign out'
+ = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
- else
%li
%div
diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml
new file mode 100644
index 00000000000..22319bba745
--- /dev/null
+++ b/app/views/layouts/koding.html.haml
@@ -0,0 +1,5 @@
+- page_title "Koding"
+- page_description "Koding Dashboard"
+- header_title "Koding", koding_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 5ee8772882e..ac04f57e217 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -9,7 +9,7 @@
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
%span
Overview
- = nav_link(controller: %w(system_info background_jobs logs health_check)) do
+ = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
= link_to admin_system_info_path, title: 'Monitoring' do
%span
Monitoring
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 52e41b1a857..67f558c854b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,64 +1,49 @@
%ul.nav.nav-sidebar
= 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
- .icon-container
- = navbar_icon('project')
%span
Projects
= nav_link(controller: :todos) do
= link_to dashboard_todos_path, title: 'Todos' do
- .icon-container
- = icon('bell fw')
%span
Todos
%span.count= number_with_delimiter(todos_pending_count)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- .icon-container
- = navbar_icon('activity')
%span
Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
- .icon-container
- = navbar_icon('group')
%span
Groups
= nav_link(controller: 'dashboard/milestones') do
= link_to dashboard_milestones_path, title: 'Milestones' do
- .icon-container
- = navbar_icon('milestones')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
= link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- .icon-container
- = navbar_icon('issues')
%span
Issues
%span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- .icon-container
- = navbar_icon('mr')
%span
Merge Requests
%span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
- = nav_link(controller: :snippets) do
+ = nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, title: 'Snippets' do
- .icon-container
- = icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
- .icon-container
- = icon('question-circle fw')
%span
Help
= nav_link(html_options: {class: profile_tab_class}) do
= link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
- .icon-container
- = icon('user fw')
%span
Profile Settings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 3b40006a0cc..e5bda7b3a6f 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,21 +1,17 @@
%ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
= link_to explore_root_path, title: 'Projects' do
- = icon('bookmark fw')
%span
Projects
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to explore_groups_path, title: 'Groups' do
- = icon('group fw')
%span
Groups
= nav_link(controller: :snippets) do
= link_to explore_snippets_path, title: 'Snippets' do
- = icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
= link_to help_path, title: 'Help' do
- = icon('question-circle fw')
%span
Help
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 96fe62c39c3..6d514f669db 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -18,9 +18,9 @@
%span
Applications
= nav_link(controller: :personal_access_tokens) do
- = link_to profile_personal_access_tokens_path, title: 'Personal Access Tokens' do
+ = link_to profile_personal_access_tokens_path, title: 'Access Tokens' do
%span
- Personal Access Tokens
+ Access Tokens
= nav_link(controller: :emails) do
= link_to profile_emails_path, title: 'Emails' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 9e65d94186b..f7012595a5a 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -65,8 +65,8 @@
Graphs
- if project_nav_tab? :issues
- = nav_link(controller: [:issues, :labels, :milestones]) do
- = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
+ = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
- if @project.default_issues_tracker?
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 51a54b4f262..52a5bdc1a1b 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -39,7 +39,7 @@
= link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
%span
Triggers
- = nav_link(controller: :badges) do
- = link_to namespace_project_badges_path(@project.namespace, @project), title: 'Badges' do
+ = nav_link(controller: :pipelines_settings) do
+ = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
%span
- Badges
+ CI/CD Pipelines
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 2049b204956..9fe94291db7 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -6,13 +6,13 @@
- content_for :scripts_body_top do
- project = @target_project || @project
- if @project_wiki && @page
- - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, params[:id])
+ - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug)
- else
- - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project)
+ - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project)
- if current_user
:javascript
window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
- window.markdown_preview_path = "#{markdown_preview_path}";
+ window.preview_markdown_path = "#{preview_markdown_path}";
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index fc64c98038b..ca5c2f2688c 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -3,3 +3,5 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_name %>
+
+<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml
new file mode 100644
index 00000000000..4f3d36bd9ca
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.html.haml
@@ -0,0 +1,12 @@
+%p
+ You have been mentioned in an issue.
+
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @issue.author_name, user_url(@issue.author)} wrote:
+-if @issue.description
+ = markdown(@issue.description, pipeline: :email, author: @issue.author)
+
+- if @issue.assignee_id.present?
+ %p
+ Assignee: #{@issue.assignee_name}
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
new file mode 100644
index 00000000000..457e94b4800
--- /dev/null
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -0,0 +1,7 @@
+You have been mentioned in an issue.
+
+Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %>
+Author: <%= @issue.author_name %>
+Assignee: <%= @issue.assignee_name %>
+
+<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.html.haml b/app/views/notify/new_mention_in_merge_request_email.html.haml
new file mode 100644
index 00000000000..32aedb9e6b9
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.html.haml
@@ -0,0 +1,15 @@
+%p
+ You have been mentioned in Merge Request #{@merge_request.to_reference}
+
+- if current_application_settings.email_author_in_body
+ %div
+ #{link_to @merge_request.author_name, user_url(@merge_request.author)} wrote:
+%p.details
+ != merge_path_description(@merge_request, '&rarr;')
+
+- if @merge_request.assignee_id.present?
+ %p
+ Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
+
+-if @merge_request.description
+ = markdown(@merge_request.description, pipeline: :email, author: @merge_request.author)
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
new file mode 100644
index 00000000000..5bf0282e097
--- /dev/null
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -0,0 +1,9 @@
+You have been mentioned in Merge Request <%= @merge_request.to_reference %>
+
+<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
+
+<%= merge_path_description(@merge_request, 'to') %>
+Author: <%= @merge_request.author_name %>
+Assignee: <%= @merge_request.assignee_name %>
+
+<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index d4aad8d1862..3c8f178ac77 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -6,3 +6,5 @@ New Merge Request <%= @merge_request.to_reference %>
Author: <%= @merge_request.author_name %>
Assignee: <%= @merge_request.assignee_name %>
+<%= @merge_request.description %>
+
diff --git a/app/views/notify/note_merge_request_email.html.haml b/app/views/notify/note_merge_request_email.html.haml
index 35c4b862bb7..ea7e3d199fd 100644
--- a/app/views/notify/note_merge_request_email.html.haml
+++ b/app/views/notify/note_merge_request_email.html.haml
@@ -1,4 +1,4 @@
-- if @note.diff_note?
+- if @note.diff_note? && @note.diff_file
%p.details
New comment on diff for
= link_to @note.diff_file.file_path, @target_url
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index c161ecc3463..c0c07d65daa 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -75,8 +75,7 @@
- blob = diff_file.blob
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white
- - diff_file.highlighted_diff_lines.each do |line|
- = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
+ = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, plain: true, email: true }
- else
No preview for this file type
%br
diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml
new file mode 100644
index 00000000000..522421b7cc3
--- /dev/null
+++ b/app/views/notify/resolved_all_discussions_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name}
diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb
new file mode 100644
index 00000000000..b0d380af8fc
--- /dev/null
+++ b/app/views/notify/resolved_all_discussions_email.text.erb
@@ -0,0 +1,3 @@
+All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %>
+
+<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
index 003884a5bd9..943ebdaeffe 100644
--- a/app/views/profiles/_head.html.haml
+++ b/app/views/profiles/_head.html.haml
@@ -1,3 +1,3 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/cropper.js')
- = page_specific_javascript_tag('profile/application.js')
+ = page_specific_javascript_tag('profile/profile_bundle.js')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 57d16d29158..c80f22457b4 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -70,7 +70,7 @@
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
- else
- = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do
Connect
%hr
- if current_user.can_change_username?
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 6a067a03535..a42b3b8eb38 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -11,7 +11,7 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
+ = link_to "generate it.", help_page_path("ssh/README")
= render 'form'
%hr
%h5
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 71ac367830d..05a2ea67aa2 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -7,6 +7,10 @@
= page_title
%p
You can generate a personal access token for each application you use that needs access to the GitLab API.
+ %p
+ You can also use personal access tokens to authenticate against Git over HTTP.
+ They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
+
.col-lg-9
- if flash[:personal_access_token]
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index b4d35dc9a3e..2afa026847a 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -43,12 +43,12 @@
.form-group
= f.label :dashboard, class: 'label-light' do
Default Dashboard
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
+ = link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank')
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group
= f.label :project_view, class: 'label-light' do
Project view
- = link_to('(?)', help_page_path('profile', 'preferences') + '#default-project-view', target: '_blank')
+ = link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank')
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
Choose what content you want to see on a project's home page.
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 5890456bee2..03ac739ade5 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -2,6 +2,10 @@
- header_title "Two-Factor Authentication", profile_two_factor_auth_path
= render 'profiles/head'
+- if inject_u2f_api?
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('u2f.js')
+
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
@@ -14,7 +18,7 @@
- else
%p
Download the Google Authenticator application from App Store or Google Play Store and scan this code.
- More information is available in the #{link_to('documentation', help_page_path('profile', 'two_factor_authentication'))}.
+ More information is available in the #{link_to('documentation', help_page_path('profile/two_factor_authentication'))}.
.row.append-bottom-10
.col-md-3
= raw @qr_code
@@ -56,13 +60,38 @@
two-factor authentication app before a U2F device. That way you'll always be able to
log in - even when you're using an unsupported browser.
.col-lg-9
- %p
- - if @registration_key_handles.present?
- = icon "check inverse", base: "circle", class: "text-success", text: "You have #{pluralize(@registration_key_handles.size, 'U2F device')} registered with GitLab."
- if @u2f_registration.errors.present?
= form_errors(@u2f_registration)
= render "u2f/register"
+ %hr
+
+ %h5 U2F Devices (#{@u2f_registrations.length})
+
+ - if @u2f_registrations.present?
+ .table-responsive
+ %table.table.table-bordered.u2f-registrations
+ %colgroup
+ %col{ width: "50%" }
+ %col{ width: "30%" }
+ %col{ width: "20%" }
+ %thead
+ %tr
+ %th Name
+ %th Registered On
+ %th
+ %tbody
+ - @u2f_registrations.each do |registration|
+ %tr
+ %td= registration.name.presence || "<no name set>"
+ %td= registration.created_at.to_date.to_s(:medium)
+ %td= link_to "Delete", profile_u2f_registration_path(registration), method: :delete, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to delete this device? This action cannot be undone." }
+
+ - else
+ .settings-message.text-center
+ You don't have any U2F devices registered yet.
+
+
- if two_factor_skippable?
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 48b0dd6b121..ac50ce83f6a 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -5,7 +5,8 @@
%i.fa.fa-rss
= render 'shared/event_filter'
-.content_list{:"data-href" => activity_project_path(@project)}
+
+.content_list.project-activity{:"data-href" => activity_project_path(@project)}
= spinner
:javascript
diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml
index 2987f6b5b22..e74fd5b93ea 100644
--- a/app/views/projects/_bitbucket_import_modal.html.haml
+++ b/app/views/projects/_bitbucket_import_modal.html.haml
@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
- == #{link_to 'OAuth integration', help_page_path("integration", "bitbucket")}.
+ == #{link_to 'OAuth integration', help_page_path("integration/bitbucket")}.
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
deleted file mode 100644
index 0568c2d305e..00000000000
--- a/app/views/projects/_builds_settings.html.haml
+++ /dev/null
@@ -1,65 +0,0 @@
-%fieldset.builds-feature
- %h5.prepend-top-0
- Builds
- - unless @repository.gitlab_ci_yml
- .form-group
- %p Builds need to be configured before you can begin using Continuous Integration.
- = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
- .form-group
- %p Get recent application code using the following command:
- .radio
- = f.label :build_allow_git_fetch_false do
- = f.radio_button :build_allow_git_fetch, 'false'
- %strong git clone
- %br
- %span.descr Slower but makes sure you have a clean dir before every build
- .radio
- = f.label :build_allow_git_fetch_true do
- = f.radio_button :build_allow_git_fetch, 'true'
- %strong git fetch
- %br
- %span.descr Faster
-
- .form-group
- = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
- = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
- .form-group
- = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
- .input-group
- %span.input-group-addon /
- = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- %p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
- .bs-callout.bs-callout-info
- %p Below are examples of regex for existing tools:
- %ul
- %li
- Simplecov (Ruby) -
- %code \(\d+.\d+\%\) covered
- %li
- pytest-cov (Python) -
- %code \d+\%\s*$
- %li
- phpunit --coverage-text --colors=never (PHP) -
- %code ^\s*Lines:\s*\d+.\d+\%
- %li
- gcovr (C/C++) -
- %code ^TOTAL.*\s+(\d+\%)$
- %li
- tap --coverage-report=text-summary (Node.js) -
- %code ^Statements\s*:\s*([^%]+)
-
- .form-group
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public builds
- .help-block Allow everyone to access builds for Public and Internal projects
-
- .form-group.append-bottom-0
- = f.label :runners_token, "Runners token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml
index 377cf0187b8..e9f39b16aa7 100644
--- a/app/views/projects/_gitlab_import_modal.html.haml
+++ b/app/views/projects/_gitlab_import_modal.html.haml
@@ -10,4 +10,4 @@
as administrator you need to configure
- else
ask your GitLab administrator to configure
- == #{link_to 'OAuth integration', help_page_path("integration", "gitlab")}.
+ == #{link_to 'OAuth integration', help_page_path("integration/gitlab")}.
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index cf11723dc8e..8ef31ca3bda 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,7 @@
- empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
%div{ class: container_class }
- = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70')
+ = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile')
%h1.project-title
= @project.name
%span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
@@ -24,6 +24,3 @@
.project-clone-holder
= render "shared/clone_panel"
-
-:javascript
- new Star();
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 771a2e0df7d..19b4249374b 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -8,4 +8,4 @@
%strong Only allow merge requests to be merged if the build succeeds
.help-block
Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('workflow', 'merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+ = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 413477a2d3a..3978fa60d66 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,7 +1,8 @@
+- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.zen-backdrop
- classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
- = f.text_area attr, class: classes, placeholder: placeholder
+ = f.text_area attr, class: classes, placeholder: placeholder, data: { supports_slash_commands: supports_slash_commands }
- else
= text_area_tag attr, nil, class: classes, placeholder: placeholder
%a.zen-control.zen-control-leave.js-zen-leave{ href: "#" }
diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb
new file mode 100644
index 00000000000..a5fef4fc56f
--- /dev/null
+++ b/app/views/projects/badges/badge.svg.erb
@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="<%= badge.width %>" height="20">
+ <linearGradient id="b" x2="0" y2="100%">
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
+ <stop offset="1" stop-opacity=".1"/>
+ </linearGradient>
+
+ <mask id="a">
+ <rect width="<%= badge.width %>" height="20" rx="3" fill="#fff"/>
+ </mask>
+
+ <g mask="url(#a)">
+ <path fill="<%= badge.key_color %>"
+ d="M0 0 h<%= badge.key_width %> v20 H0 z"/>
+ <path fill="<%= badge.value_color %>"
+ d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/>
+ <path fill="url(#b)"
+ d="M0 0 h<%= badge.width %> v20 H0 z"/>
+ </g>
+
+ <g fill="#fff" text-anchor="middle">
+ <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
+ <text x="<%= badge.key_text_anchor %>" y="15" fill="#010101" fill-opacity=".3">
+ <%= badge.key_text %>
+ </text>
+ <text x="<%= badge.key_text_anchor %>" y="14">
+ <%= badge.key_text %>
+ </text>
+ <text x="<%= badge.value_text_anchor %>" y="15" fill="#010101" fill-opacity=".3">
+ <%= badge.value_text %>
+ </text>
+ <text x="<%= badge.value_text_anchor %>" y="14">
+ <%= badge.value_text %>
+ </text>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
deleted file mode 100644
index ac80951dd4f..00000000000
--- a/app/views/projects/badges/index.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-- page_title 'Badges'
-- badges_path = namespace_project_badges_path(@project.namespace, @project)
-
-.prepend-top-10
- .panel.panel-default
- .panel-heading
- %b Builds badge &middot;
- = @build_badge.to_html
- .pull-right
- = render 'shared/ref_switcher', destination: 'badges', align_right: true
- .panel-body
- .row
- .col-md-2.text-center
- Markdown
- .col-md-10.code.js-syntax-highlight
- = highlight('.md', @build_badge.to_markdown)
- .row
- %hr
- .row
- .col-md-2.text-center
- HTML
- .col-md-10.code.js-syntax-highlight
- = highlight('.html', @build_badge.to_html)
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index cdac50f7a8d..ff893ea74e1 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -16,6 +16,7 @@
- if current_user
.btn-group{ role: "group" }
- = edit_blob_link
+ - if blob_text_viewable?(@blob)
+ = edit_blob_link
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 29c7d45074a..0237e152b54 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -4,7 +4,9 @@
= icon('code-fork')
= ref
%span.editor-file-name
- = @path
+ - if current_action?(:edit) || current_action?(:update)
+ = text_field_tag 'file_path', (params[:file_path] || @path),
+ class: 'form-control new-file-path'
- if current_action?(:new) || current_action?(:create)
%span.editor-file-name
@@ -22,7 +24,7 @@
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
- .file-content.code
+ .file-editor.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index 18caddabd39..4c356d1f07f 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -1,9 +1,15 @@
.file-content.image_file
- if blob.svg?
- - # We need to scrub SVG but we cannot do so in the RawController: it would
- - # be wrong/strange if RawController modified the data.
- - blob.load_all_data!(@repository)
- - blob = sanitize_svg(blob)
- %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - if blob.size_within_svg_limits?
+ - # We need to scrub SVG but we cannot do so in the RawController: it would
+ - # be wrong/strange if RawController modified the data.
+ - blob.load_all_data!(@repository)
+ - blob = sanitize_svg(blob)
+ %img{src: "data:#{blob.mime_type};base64,#{Base64.encode64(blob.data)}"}
+ - else
+ .nothing-here-block
+ The SVG could not be displayed as it is too large, you can
+ #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+ instead.
- else
%img{src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path))}
diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml
index 5926d181ba3..a79ae53c780 100644
--- a/app/views/projects/blob/diff.html.haml
+++ b/app/views/projects/blob/diff.html.haml
@@ -1,20 +1,30 @@
- if @lines.present?
+ - line_class = diff_view == :inline ? '' : diff_view
- if @form.unfold? && @form.since != 1 && !@form.bottom?
- %tr.line_holder
- = render "projects/diffs/match_line", { line: @match_line,
- line_old: @form.since, line_new: @form.since, bottom: false, new_file: false }
+ %tr.line_holder{ class: line_class }
+ = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view
- @lines.each_with_index do |line, index|
- line_new = index + @form.since
- line_old = line_new - @form.offset
- %tr.line_holder{ id: line_old }
- %td.old_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_old), "##{line_old}"
- %td.new_line.diff-line-num{ data: { linenumber: line_old } }
- = link_to raw(line_new) , "##{line_old}"
- %td.line_content.noteable_line==#{' ' * @form.indent}#{line}
+ - line_content = capture do
+ %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line}
+ %tr.line_holder{ id: line_old, class: line_class }
+ - case diff_view
+ - when :inline
+ %td.old_line.diff-line-num{ data: { linenumber: line_old } }
+ %a{href: "##{line_old}", data: { linenumber: line_old }}
+ %td.new_line.diff-line-num{ data: { linenumber: line_new } }
+ %a{href: "##{line_new}", data: { linenumber: line_new }}
+ = line_content
+ - when :parallel
+ %td.old_line.diff-line-num{data: { linenumber: line_old }}
+ = link_to raw(line_old), "##{line_old}"
+ = line_content
+ %td.new_line.diff-line-num{data: { linenumber: line_new }}
+ = link_to raw(line_new), "##{line_new}"
+ = line_content
- if @form.unfold? && @form.bottom? && @form.to < @blob.loc
- %tr.line_holder{ id: @form.to }
- = render "projects/diffs/match_line", { line: @match_line,
- line_old: @form.to, line_new: @form.to, bottom: true, new_file: false }
+ %tr.line_holder{ id: @form.to, class: line_class }
+ = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index b1c9895f43e..680e95ac6b5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,4 +1,13 @@
- page_title "Edit", @blob.path, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
+
+- if @conflict
+ .alert.alert-danger
+ Someone edited the file the same time you did. Please check out
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs.
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
@@ -10,15 +19,10 @@
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
-
- = hidden_field_tag 'last_commit', @last_commit
+ = 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]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
- new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index c952bc7e5db..b6ed9518c48 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,17 +1,16 @@
- page_title "New File", @path.presence, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
%h3.page-title
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
- new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
new file mode 100644
index 00000000000..97eb952eff1
--- /dev/null
+++ b/app/views/projects/boards/components/_blank_state.html.haml
@@ -0,0 +1,15 @@
+%board-blank-state{ "inline-template" => true,
+ "v-if" => "list.id == 'blank'" }
+ .board-blank-state
+ %p
+ Add the following default lists to your Issue Board with one click:
+ %ul.board-blank-state-list
+ %li{ "v-for" => "label in predefinedLabels" }
+ %span.label-color{ ":style" => "{ backgroundColor: label.color } " }
+ {{ label.title }}
+ %p
+ Starting out with the default set of lists will get you right on the way to making the most of your board.
+ %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
+ Add default lists
+ %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
+ Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
new file mode 100644
index 00000000000..de53a298f84
--- /dev/null
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -0,0 +1,43 @@
+%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) }" }
+ {{ list.title }}
+ %span.pull-right{ "v-if" => "list.type !== 'blank'" }
+ {{ list.issues.length }}
+ - 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")
+ = icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
+ .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
+ %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
+ = icon("search", class: "board-search-icon", "v-show" => "!query")
+ %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
+ = icon("times", class: "board-search-clear")
+ %board-list{ "inline-template" => true,
+ "v-if" => "list.type !== 'blank'",
+ ":list" => "list",
+ ":issues" => "list.issues",
+ ":loading" => "list.loading",
+ ":disabled" => "disabled",
+ ":issue-link-base" => "issueLinkBase" }
+ .board-list-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ %ul.board-list{ "v-el:list" => true,
+ "v-show" => "!loading",
+ ":data-board" => "list.id" }
+ = render "projects/boards/components/card"
+ - if can?(current_user, :admin_list, @project)
+ = render "projects/boards/components/blank_state"
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
new file mode 100644
index 00000000000..e8b60b54d80
--- /dev/null
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -0,0 +1,33 @@
+%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 }",
+ ":index" => "index" }
+ %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
+ = precede '#' do
+ {{ issue.id }}
+ %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 }}
+ %a.has-tooltip{ ":href" => "'/u/' + 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 }
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
new file mode 100644
index 00000000000..edbbd3f3d2a
--- /dev/null
+++ b/app/views/projects/boards/show.html.haml
@@ -0,0 +1,19 @@
+- @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
+
+.boards-list#board-app{ "v-cloak" => true,
+ "data-endpoint" => "#{namespace_project_board_path(@project.namespace, @project)}",
+ "data-disabled" => "#{!can?(current_user, :admin_list, @project)}",
+ "data-issue-link-base" => "#{namespace_project_issues_path(@project.namespace, @project)}" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 4bd85061240..6192ccb710b 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -5,8 +5,8 @@
- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
- = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %span.item-title.str-truncated= branch.name
+ = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
+ = branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index 9fe65cbb104..d54c76ff9c8 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,5 +1,5 @@
.branch-commit
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace"
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-id monospace"
&middot;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 77b405f1f39..e889f29c816 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -7,28 +7,32 @@
.nav-text
Protected branches can be managed in project settings
- - if can? current_user, :push_code, @project
- .nav-controls
+ .nav-controls
+ = form_tag(filter_branches_path, method: :get) do
+ = 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'}
+ %span.light
+ = projects_sort_options_hash[@sort]
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ %li
+ = link_to filter_branches_path(sort: sort_value_name) do
+ = sort_title_name
+ = link_to filter_branches_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to filter_branches_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
+
+ - if can? current_user, :push_code, @project
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
New branch
- .dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = @sort.humanize
- - else
- Name
- %b.caret
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to namespace_project_branches_path(sort: nil) do
- Name
- = link_to namespace_project_branches_path(sort: 'recently_updated') do
- = sort_title_recently_updated
- = link_to namespace_project_branches_path(sort: 'last_updated') do
- = sort_title_oldest_updated
+
- if @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch
= paginate @branches, theme: 'gitlab'
+ - else
+ .nothing-here-block No branches to show
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index cab21f0cf19..5b0b58e087b 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -11,97 +11,133 @@
%p.build-detail-row
#{@build.coverage}%
- - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
- .block{ class: ("block-first" if !@build.coverage) }
- .title
- Build artifacts
- - if @build.artifacts_expired?
- %p.build-detail-row
- The artifacts were removed
- #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.artifacts_expire_at
- %p.build-detail-row
- The artifacts will be removed in
- %span.js-artifacts-remove= @build.artifacts_expire_at
+ - builds = @build.pipeline.builds.latest.to_a
+ - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+ - if builds.size > 1
+ .dropdown.build-dropdown
+ .build-light-text Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ = icon('caret-down')
+ %ul.dropdown-menu
+ - builds.map(&:stage).uniq.each do |stage|
+ %li
+ %a.stage-item= stage
- - if @build.artifacts?
- .btn-group.btn-group-justified{ role: :group }
- - if @build.artifacts_expire_at
- = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
- Keep
+ .builds-container
+ - statuses.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = icon('check')
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Download
+ - if @build.retried?
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning
+ This build was retried.
- - if @build.artifacts_metadata?
- = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
- Browse
+ .blocks-container
+ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+ .block{ class: ("block-first" if !@build.coverage) }
+ .title
+ Build artifacts
+ - if @build.artifacts_expired?
+ %p.build-detail-row
+ The artifacts were removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+ - elsif @build.artifacts_expire_at
+ %p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove= @build.artifacts_expire_at
- .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
- .title
- Build details
- - if @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
- - if @build.merge_request
- %p.build-detail-row
- %span.build-light-text Merge Request:
- = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
- - if @build.duration
- %p.build-detail-row
- %span.build-light-text Duration:
- #{duration_in_words(@build.finished_at, @build.started_at)}
- - if @build.finished_at
- %p.build-detail-row
- %span.build-light-text Finished:
- #{time_ago_with_tooltip(@build.finished_at)}
- - if @build.erased_at
- %p.build-detail-row
- %span.build-light-text Erased:
- #{time_ago_with_tooltip(@build.erased_at)}
- %p.build-detail-row
- %span.build-light-text Runner:
- - if @build.runner && current_user && current_user.admin
- = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- - elsif @build.runner
- \##{@build.runner.id}
- .btn-group.btn-group-justified{ role: :group }
- - if @build.has_trace?
- = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- - if @build.active?
- = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
- - if can?(current_user, :update_build, @project) && @build.erasable?
- = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
- class: "btn btn-sm btn-default", method: :post,
- data: { confirm: "Are you sure you want to erase this build?" } do
- Erase
+ - if @build.artifacts?
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.artifacts_expire_at
+ = link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
+ Keep
- - if @build.trigger_request
- .build-widget
- %h4.title
- Trigger
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Download
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
+ - if @build.artifacts_metadata?
+ = link_to browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ Browse
+
+ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) }
+ .title
+ Build details
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ - if @build.merge_request
+ %p.build-detail-row
+ %span.build-light-text Merge Request:
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
+ - if @build.duration
+ %p.build-detail-row
+ %span.build-light-text Duration:
+ = time_interval_in_words(@build.duration)
+ - if @build.finished_at
+ %p.build-detail-row
+ %span.build-light-text Finished:
+ #{time_ago_with_tooltip(@build.finished_at)}
+ - if @build.erased_at
+ %p.build-detail-row
+ %span.build-light-text Erased:
+ #{time_ago_with_tooltip(@build.erased_at)}
+ %p.build-detail-row
+ %span.build-light-text Runner:
+ - if @build.runner && current_user && current_user.admin
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
+ - elsif @build.runner
+ \##{@build.runner.id}
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
+ - if @build.active?
+ = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
+ - if can?(current_user, :update_build, @project) && @build.erasable?
+ = link_to erase_namespace_project_build_path(@project.namespace, @project, @build),
+ class: "btn btn-sm btn-default", method: :post,
+ data: { confirm: "Are you sure you want to erase this build?" } do
+ Erase
+
+ - if @build.trigger_request
+ .build-widget
+ %h4.title
+ Trigger
- - if @build.trigger_request.variables
%p
- %span.build-light-text Variables:
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
+
+ - if @build.trigger_request.variables
+ %p
+ %span.build-light-text Variables:
- %code
- - @build.trigger_request.variables.each do |key, value|
- #{key}=#{value}
- .block
- .title
- Commit message
- %p.build-light-text.append-bottom-0
- #{@build.pipeline.git_commit_message}
+ - @build.trigger_request.variables.each do |key, value|
+ %code
+ #{key}=#{value}
- - if @build.tags.any?
.block
.title
- Tags
- - @build.tag_list.each do |tag|
- %span.label.label-primary
- = tag
+ Commit title
+ %p.build-light-text.append-bottom-0
+ #{@build.pipeline.git_commit_title}
+
+ - if @build.tags.any?
+ .block
+ .title
+ Tags
+ - @build.tag_list.each do |tag|
+ %span.label.label-primary
+ = tag
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index a131289ee97..2af625f69cd 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -11,17 +11,22 @@
%span.badge.js-totalbuilds-count
= number_with_delimiter(@all_builds.count(:id))
+ %li{class: ('active' if @scope == 'pending')}
+ = link_to project_builds_path(@project, scope: :pending) do
+ Pending
+ %span.badge
+ = number_with_delimiter(@all_builds.pending.count(:id))
%li{class: ('active' if @scope == 'running')}
= link_to project_builds_path(@project, scope: :running) do
Running
- %span.badge.js-running-count
- = number_with_delimiter(@all_builds.running_or_pending.count(:id))
+ %span.badge
+ = number_with_delimiter(@all_builds.running.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to project_builds_path(@project, scope: :finished) do
Finished
- %span.badge.js-running-count
+ %span.badge
= number_with_delimiter(@all_builds.finished.count(:id))
.nav-controls
@@ -31,12 +36,12 @@
data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+ = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %ul.content-list
+ %ul.content-list.builds-content-list
- if @builds.blank?
%li
.nothing-here-block No builds to show
@@ -46,14 +51,10 @@
%thead
%tr
%th Status
- %th Build ID
%th Commit
- %th Ref
%th Stage
%th Name
- %th Tags
- %th Duration
- %th Finished at
+ %th
- if @project.build_coverage_enabled?
%th Coverage
%th
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 4e801cc72fe..e4d41288aa6 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -5,26 +5,6 @@
.build-page
= render "header"
- - builds = @build.pipeline.builds.latest.to_a
- - if builds.size > 1
- %ul.nav-links.no-top.no-bottom
- - builds.each do |build|
- %li{class: ('active' if build == @build) }
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
-
- - if @build.retried?
- %li.active
- %a
- Build ##{@build.id}
- &middot;
- %i.fa.fa-warning
- This build was retried.
- if @build.stuck?
- unless @build.any_runners_online?
.bs-callout.bs-callout-warning
@@ -67,4 +47,10 @@
= render "sidebar"
:javascript
- new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{namespace_project_build_url(@project.namespace, @project, @build, :json)}", "#{@build.status}", "#{trace_with_state[:state]}")
+ 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]}"
+ })
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 16b8e1cca91..ca907077c2b 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -9,7 +9,7 @@
- if can_create_issue
%li
- = link_to url_for_new_issue(@project, only_path: true) do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project) do
= icon('exclamation-circle fw')
New issue
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index a9eaed4c5f6..d78888e9fe4 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -2,17 +2,13 @@
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
- = icon('code-fork fw')
+ = custom_icon('icon_fork')
Fork
- %div.count-with-arrow
- %span.arrow
- %span.count
- = @project.forks_count
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
- = icon('code-fork fw')
+ = custom_icon('icon_fork')
Fork
- %div.count-with-arrow
- %span.arrow
- = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
- = @project.forks_count
+ %div.count-with-arrow
+ %span.arrow
+ = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
+ = @project.forks_count
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
new file mode 100644
index 00000000000..fdc80d44253
--- /dev/null
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -0,0 +1,7 @@
+- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
+ - if @repository.koding_yml
+ = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
+ Run in IDE (Koding)
+ - else
+ = link_to add_koding_stack_path(@project), class: 'btn' do
+ Set Up Koding
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 71cf5582a4c..311583037e5 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,10 @@
- if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
- if current_user.starred?(@project)
- = icon('star fw')
+ = icon('star')
%span.starred Unstar
- else
- = icon('star-o fw')
+ = icon('star-o')
%span Star
%div.count-with-arrow
%span.arrow
@@ -13,7 +13,7 @@
- else
= link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: 'You must sign in to star a project' do
- = icon('star fw')
+ = icon('star')
Star
%div.count-with-arrow
%span.arrow
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 5bd6e3f0ebc..1fdf32466f2 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -1,32 +1,49 @@
-%tr.build
+%tr.build.commit
%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)
- %td.build-link
- - if can?(current_user, :read_build, build)
- = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %strong ##{build.id}
- - else
- %strong ##{build.id}
+ %td
+ .branch-commit
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
+ %span ##{build.id}
+ - else
+ %span ##{build.id}
- - if build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- - if defined?(retried) && retried
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+ - if defined?(ref) && ref
+ - if build.ref
+ .icon-container
+ = build.tag? ? icon('tag') : icon('code-fork')
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
+ - else
+ .light none
+ .icon-container
+ = custom_icon("icon_commit")
- - if defined?(commit_sha) && commit_sha
- %td
- = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace"
+ - if defined?(commit_sha) && commit_sha
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- - if defined?(ref) && ref
- %td
- - if build.ref
- = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref)
- - else
- .light none
+ - if build.stuck?
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ - if defined?(retried) && retried
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+
+ .label-container
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+ - if defined?(retried) && retried
+ %span.label.label-warning retried
+ - if build.manual?
+ %span.label.label-info manual
- if defined?(runner) && runner
%td
@@ -43,25 +60,14 @@
= build.name
%td
- .label-container
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.try(:trigger_request)
- %span.label.label-info triggered
- - if build.try(:allow_failure)
- %span.label.label-danger allowed to fail
- - if defined?(retried) && retried
- %span.label.label-warning retried
-
- %td.duration
- if build.duration
- #{duration_in_words(build.finished_at, build.started_at)}
-
- %td.timestamp
+ %p.duration
+ = custom_icon("icon_timer")
+ = duration_in_numbers(build.duration)
- if build.finished_at
- %span #{time_ago_with_tooltip(build.finished_at)}
+ %p.finished-at
+ = icon("calendar")
+ %span #{time_ago_with_tooltip(build.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
@@ -77,6 +83,10 @@
- if build.active?
= 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 defined?(allow_retry) && allow_retry && 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('refresh')
+ - elsif defined?(allow_retry) && 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?
+ = 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
+ = icon('play')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
new file mode 100644
index 00000000000..36fb0300aeb
--- /dev/null
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -0,0 +1,15 @@
+- is_playable = subject.playable? && can?(current_user, :update_build, @project)
+%li.build{class: ("playable" if is_playable)}
+ .curve
+ .build-content
+ - if is_playable
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = render_status_with_link('build', 'play')
+ %span.ci-status-text= subject.name
+ - elsif can?(current_user, :read_build, @project)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = render_status_with_link('build', subject.status)
+ %span.ci-status-text= subject.name
+ - else
+ = render_status_with_link('build', subject.status)
+ = 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 af8dd5cd02c..b119f6edf14 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,20 +1,24 @@
- status = pipeline.status
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: "ci-status ci-#{status}" do
- = ci_icon_for_status(status)
- %strong ##{pipeline.id}
-
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
+ - if defined?(status_icon_only) && status_icon_only
+ = ci_icon_for_status(status)
+ - else
+ = ci_status_with_icon(status)
%td
- %div.branch-commit
+ .branch-commit
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
+ %span ##{pipeline.id}
- if pipeline.ref
- = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace"
- &middot;
- = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
- &nbsp;
- - if pipeline.tag?
- %span.label.label-primary tag
- - elsif pipeline.latest?
+ - unless defined?(hide_branch) && hide_branch
+ .icon-container
+ = pipeline.tag? ? icon('tag') : icon('code-fork')
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
+ .icon-container
+ = custom_icon("icon_commit")
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
+ - if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
@@ -25,47 +29,68 @@
%p.commit-title
- if commit = pipeline.commit
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
+ = author_avatar(commit, size: 20)
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
- - stages_status = pipeline.statuses.stages_status
+ - stages_status = pipeline.statuses.relevant.latest.stages_status
- stages.each do |stage|
- %td
+ %td.stage-cell
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
- = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = 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)
- else
.light.has-tooltip{ title: tooltip }
\-
%td
- - if pipeline.started_at && pipeline.finished_at
+ - if pipeline.duration
%p.duration
- = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+ = custom_icon("icon_timer")
+ = duration_in_numbers(pipeline.duration)
+ - if pipeline.finished_at
+ %p.finished-at
+ = icon("calendar")
+ #{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
- %td
+ %td.pipeline-actions
.controls.hidden-xs.pull-right
- - artifacts = pipeline.builds.latest.select { |b| b.artifacts? }
- - if artifacts.present?
- .dropdown.inline.build-artifacts
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- = icon('download')
- %b.caret
- %ul.dropdown-menu.dropdown-menu-align-right
- - artifacts.each do |build|
- %li
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
- = icon("download")
- %span Download '#{build.name}' artifacts
+ - artifacts = pipeline.builds.latest.with_artifacts_not_expired
+ - actions = pipeline.manual_actions
+ - if artifacts.present? || actions.any?
+ .btn-group.inline
+ - if actions.any?
+ .btn-group
+ %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ = icon("play")
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - actions.each do |build|
+ %li
+ = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
+ = icon("play")
+ %span= build.name.humanize
+ - if artifacts.present?
+ .btn-group
+ %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
+ = icon("download")
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - artifacts.each do |build|
+ %li
+ = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do
+ = icon("download")
+ %span Download '#{build.name}' artifacts
- - if can?(current_user, :update_pipeline, @project)
- - if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
- = icon("repeat")
- - if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
- = icon("remove")
+ - if can?(current_user, :update_pipeline, pipeline.project)
+ .cancel-retry-btns.inline
+ - if pipeline.retryable?
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = icon("repeat")
+ - if pipeline.cancelable?
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = icon("remove")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index d9b800a4ded..e4cd55b9f7a 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,9 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
- = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
+ = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }})
+
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index ea33aa472a6..935433306ea 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -2,7 +2,7 @@
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
- %span.badge= @diffs.count
+ %span.badge= @diffs.size
= nav_link(path: 'commit#builds') do
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Builds
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
index 9d925cacc0d..6bb900e3fc1 100644
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -8,8 +8,8 @@
- if stage
&nbsp;
= stage.titleize
- = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
+ = 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 3ad866bb2f1..29d767e7769 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -56,10 +56,10 @@
= pluralize(@commit.pipelines.count, 'pipeline')
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
= ci_icon_for_status(@commit.status)
- = ci_label_for_status(@commit.status)
- - if @commit.pipelines.duration
- in
- = time_interval_in_words @commit.pipelines.duration
+ %span.ci-status-label
+ = ci_label_for_status(@commit.status)
+ in
+ = time_interval_in_words @commit.pipelines.total_duration
.commit-box.content-block
%h3.commit-title
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 0411137b7c6..20a85148ab5 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,5 +1,9 @@
-.row-content-block.build-content.middle-block
+.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
@@ -23,6 +27,22 @@
in
= time_interval_in_words pipeline.duration
+.row-content-block.build-content.middle-block.pipeline-graph
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ - statuses.each do |status|
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+
+
- if pipeline.yaml_errors.present?
.bs-callout.bs-callout-danger
%h4 Found errors in your .gitlab-ci.yml:
@@ -35,18 +55,16 @@
.bs-callout.bs-callout-warning
\.gitlab-ci.yml not found in this commit
-.table-holder
- %table.table.builds
+.table-holder.pipeline-holder
+ %table.table.builds.pipeline
%thead
%tr
%th Status
%th Build ID
%th Name
- %th Tags
- %th Duration
- %th Finished at
+ %th
- if pipeline.project.build_coverage_enabled?
%th Coverage
%th
- - pipeline.statuses.stages.each do |stage|
- = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage)
+ - pipeline.statuses.relevant.stages.each do |stage|
+ = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
new file mode 100644
index 00000000000..29f4ef8f49e
--- /dev/null
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -0,0 +1,17 @@
+%ul.content-list.pipelines
+ - if pipelines.blank?
+ %li
+ .nothing-here-block No pipelines to show
+ - else
+ .table-holder
+ %table.table.builds
+ %tbody
+ %th Status
+ %th Commit
+ - pipelines.stages.each do |stage|
+ %th.stage
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ %th
+ %th
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index d0da2606587..ed44d86a687 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -7,7 +7,7 @@
= render "ci_menu"
- else
%div.block-connector
-= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs
+= render "projects/diffs/diffs", diffs: @diffs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 929496f81d8..fd888f41b1e 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,7 +9,8 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
- = commit_author_avatar(commit, size: 36)
+ = author_avatar(commit, size: 36)
+
.commit-info-block
.commit-row-title
%span.item-title
@@ -18,14 +19,15 @@
&middot;
= commit.short_id
- if commit.status
- = render_commit_status(commit, cssclass: 'visible-xs-inline')
+ .visible-xs-inline
+ = render_commit_status(commit)
- if commit.description?
%a.text-expander.hidden-xs.js-toggle-button ...
.commit-actions.hidden-xs
- if commit.status
- = render_commit_status(commit, cssclass: 'btn btn-transparent')
- = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent')
+ = render_commit_status(commit)
+ = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 61152649907..4d1ee1c5318 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,8 +1,5 @@
.scrolling-tabs-container.sub-nav-scroll
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
+ = render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index dd590a4b8ec..d79336f5a60 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,16 +1,18 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
- = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
- .form-group
+ = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+ .form-group.dropdown.compare-form-group.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
- = text_field_tag :from, params[:from], class: "form-control", required: true
+ = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence }
+ = render "ref_dropdown"
= "..."
- .form-group
+ .form-group.dropdown.compare-form-group.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
- = text_field_tag :to, params[:to], class: "form-control", required: true
+ = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence }
+ = render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- if @merge_request.present?
@@ -19,11 +21,3 @@
= link_to create_mr_path, class: 'prepend-left-10 btn' do
= icon("plus")
Create Merge Request
-
-:javascript
- var availableTags = #{@project.repository.ref_names.to_json};
-
- $("#from, #to").autocomplete({
- source: availableTags,
- minLength: 1
- });
diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml
new file mode 100644
index 00000000000..c604c6d0135
--- /dev/null
+++ b/app/views/projects/compare/_ref_dropdown.html.haml
@@ -0,0 +1,4 @@
+.dropdown-menu.dropdown-menu-selectable
+ = dropdown_title "Select branch/tag"
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 28a50e7031a..819e9bc15ae 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -8,7 +8,7 @@
- if @commits.present?
= render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
+ = render "projects/diffs/diffs", diffs: @diffs
- else
.light-well
.center
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index 894c36a96df..901605f7ca3 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -9,5 +9,5 @@
.form-group
%p.light.append-bottom-0
Paste a machine public key here. Read more about how to generate it
- = link_to "here", help_page_path("ssh", "README")
+ = link_to "here", help_page_path("ssh/README")
= f.submit "Add key", class: "btn-create btn"
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
new file mode 100644
index 00000000000..f7bf3b834ef
--- /dev/null
+++ b/app/views/projects/deployments/_actions.haml
@@ -0,0 +1,22 @@
+- if can?(current_user, :create_deployment, deployment) && deployment.deployable
+ .pull-right
+ - actions = deployment.manual_actions
+ - if actions.present?
+ .inline
+ .dropdown
+ %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
+ = icon("play")
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - actions.each do |action|
+ %li
+ = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
+ = icon("play")
+ %span= action.name.humanize
+
+ - if local_assigns.fetch(:allow_rollback, false)
+ = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do
+ - if deployment.last?
+ Re-deploy
+ - else
+ Rollback
diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml
index 0f9d9512d88..28813babd7b 100644
--- a/app/views/projects/deployments/_commit.html.haml
+++ b/app/views/projects/deployments/_commit.html.haml
@@ -1,12 +1,16 @@
%div.branch-commit
- if deployment.ref
- = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace"
- &middot;
+ .icon-container
+ = deployment.tag? ? icon('tag') : icon('code-fork')
+ = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name"
+ .icon-container
+ = custom_icon("icon_commit")
= link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace"
%p.commit-title
%span
- if commit_title = deployment.commit_title
+ = author_avatar(deployment.commit, size: 20)
= link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index d08dd92f1f6..cd95841ca5a 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -7,17 +7,12 @@
%td
- if deployment.deployable
- = link_to namespace_project_build_path(@project.namespace, @project, deployment.deployable) do
+ = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
+ = user_avatar(user: deployment.user, size: 20)
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
%td
#{time_ago_with_tooltip(deployment.created_at)}
%td
- - if can?(current_user, :create_deployment, deployment) && deployment.deployable
- .pull-right
- = link_to retry_namespace_project_build_path(@project.namespace, @project, deployment.deployable), method: :post, class: 'btn btn-build' do
- - if deployment.last?
- Retry
- - else
- Rollback
+ = render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
new file mode 100644
index 00000000000..d37961c4e40
--- /dev/null
+++ b/app/views/projects/diffs/_content.html.haml
@@ -0,0 +1,29 @@
+.diff-content.diff-wrap-lines
+ - # Skip all non non-supported blobs
+ - return unless blob.respond_to?(:text?)
+ - if diff_file.too_large?
+ .nothing-here-block This diff could not be displayed because it is too large.
+ - elsif blob.only_display_raw?
+ .nothing-here-block This file is too large to display.
+ - elsif blob_text_viewable?(blob)
+ - 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))
+ .nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
+ This diff is collapsed. Click to expand it.
+ - elsif diff_file.diff_lines.length > 0
+ - if diff_view == :parallel
+ = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
+ - else
+ = render "projects/diffs/text_file", diff_file: diff_file
+ - else
+ - if diff_file.mode_changed?
+ .nothing-here-block File mode changed
+ - elsif diff_file.renamed_file
+ .nothing-here-block File moved
+ - elsif blob.image?
+ - old_blob = diff_file.old_blob(diff_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 1975287faee..62aff36aadd 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,18 +1,19 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
-- if diff_view == 'parallel'
+- diff_files = diffs.diff_files
+- if diff_view == :parallel
- fluid_layout true
-- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository)
-
.content-block.oneline-block.files-changed
.inline-parallel-buttons
+ - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? }
+ = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
- = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs')
+ = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
- elsif current_controller?(:merge_requests)
- = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs')
+ = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs')
- elsif current_controller?(:compare)
- = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs')
+ = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs')
.btn-group
= inline_diff_btn
= parallel_diff_btn
@@ -21,12 +22,12 @@
- if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files
-.files
+.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
- next unless blob
- - blob.load_all_data!(project.repository) unless blob.only_display_raw?
+ - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw?
- = render 'projects/diffs/file', i: index, project: project,
- diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs
+ = render 'projects/diffs/file', index: index, 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 3b758a1ec4e..ad2eb3e504f 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-#{i}", data: diff_file_html_data(project, diff_file)}
+.diff-file.file-holder{id: "diff-#{index}", 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-#{i}"
+ = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}"
- unless diff_file.submodule?
.file-actions.hidden-xs
@@ -9,35 +9,11 @@
= icon('comment')
\
- - if editable_diff?(diff_file)
- = edit_blob_link(@merge_request.source_project,
- @merge_request.source_branch, diff_file.new_path,
- from_merge_request_id: @merge_request.id)
+ - if editable_diff?(diff_file)
+ - link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
+ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
+ blob: blob, link_opts: link_opts)
- = view_file_btn(diff_commit.id, diff_file, project)
+ = view_file_btn(diff_commit.id, diff_file.new_path, project)
- .diff-content.diff-wrap-lines
- - # Skip all non non-supported blobs
- - return unless blob.respond_to?(:text?)
- - if diff_file.too_large?
- .nothing-here-block This diff could not be displayed because it is too large.
- - elsif blob.only_display_raw?
- .nothing-here-block This file is too large to display.
- - elsif blob_text_viewable?(blob)
- - if !project.repository.diffable?(blob)
- .nothing-here-block This diff was suppressed by a .gitattributes entry.
- - elsif diff_file.diff_lines.length > 0
- - if diff_view == 'parallel'
- = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- - else
- = render "projects/diffs/text_file", diff_file: diff_file, index: i
- - else
- - if diff_file.mode_changed?
- .nothing-here-block File mode changed
- - elsif diff_file.renamed_file
- .nothing-here-block File moved
- - elsif blob.image?
- - old_blob = diff_file.old_blob(diff_commit)
- = render "projects/diffs/image", diff_file: diff_file, old_file: old_blob, file: blob, index: i
- - else
- .nothing-here-block No preview for this file type
+ = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 22cad00240a..7042e9f1fc9 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,30 +1,37 @@
+- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
-- line_code = diff_file.line_code(line)
-- position = diff_file.position(line)
- type = line.type
-%tr.line_holder{ id: line_code, class: type }
+- line_code = diff_file.line_code(line)
+%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
- case type
- when 'match'
- = render "projects/diffs/match_line", { line: line.text,
- line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file }
+ = diff_match_line line.old_pos, line.new_pos, text: line.text
- when 'nonewline'
%td.old_line.diff-line-num
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
%td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
- - link_text = type == "new" ? "&nbsp;".html_safe : line.old_pos
+ - link_text = type == "new" ? " " : line.old_pos
- if plain
= link_text
- else
- = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
-
- - if !plain && !@diff_notes_disabled && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(line_code, position)
+ %a{href: "##{line_code}", data: { linenumber: link_text }}
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- - link_text = type == "old" ? "&nbsp;".html_safe : line.new_pos
+ - link_text = type == "old" ? " " : line.new_pos
- if plain
= link_text
- else
- = link_to "", "##{line_code}", id: line_code, data: { linenumber: link_text }
- %td.line_content.noteable_line{ class: [type, line_code], data: { line_code: line_code, position: position.to_json } }= diff_line_content(line.text, type)
+ %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)
+ - else
+ = diff_line_content(line.text, type)
+
+- discussions = local_assigns.fetch(:discussions, nil)
+- if discussions && !line.meta?
+ - discussion = discussions[line_code]
+ - if discussion
+ - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
+ = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml
deleted file mode 100644
index d6dddd97879..00000000000
--- a/app/views/projects/diffs/_match_line.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-%td.old_line.diff-line-num{data: {linenumber: line_old},
- class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
- \...
-%td.new_line.diff-line-num{data: {linenumber: line_new},
- class: [unfold_bottom_class(bottom), unfold_class(!new_file)]}
- \...
-%td.line_content.match= line
diff --git a/app/views/projects/diffs/_match_line_parallel.html.haml b/app/views/projects/diffs/_match_line_parallel.html.haml
deleted file mode 100644
index 0cd888876e0..00000000000
--- a/app/views/projects/diffs/_match_line_parallel.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%td.old_line.diff-line-num
-%td.line_content.parallel.match= line
-%td.new_line.diff-line-num
-%td.line_content.parallel.match= line
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 51f207dce94..28aad3f4725 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,41 +1,41 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight
+%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
%table
+ - last_line = 0
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
+ - last_line = right.new_pos if right
%tr.line_holder.parallel
- - if left[:type] == 'match'
- = render "projects/diffs/match_line_parallel", { line: left[:text],
- line_old: left[:number], line_new: right[:number] }
- - elsif left[:type] == 'nonewline'
- %td.old_line.diff-line-num
- %td.line_content.parallel.match= left[:text]
- %td.new_line.diff-line-num
- %td.line_content.parallel.match= left[:text]
+ - if left
+ - if left.meta?
+ = diff_match_line left.old_pos, nil, text: left.text, view: :parallel
+ - else
+ - left_line_code = diff_file.line_code(left)
+ - left_position = diff_file.position(left)
+ %td.old_line.diff-line-num{id: left_line_code, class: left.type, data: { linenumber: left.old_pos }}
+ %a{href: "##{left_line_code}" }= raw(left.old_pos)
+ %td.line_content.parallel.noteable_line{class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old')}= diff_line_content(left.text)
- else
- %td.old_line.diff-line-num{id: left[:line_code], class: [left[:type], ('empty-cell' unless left[:number])]}
- = link_to raw(left[:number]), "##{left[:line_code]}", id: left[:line_code]
- - if !@diff_notes_disabled && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(left[:line_code], left[:position], 'old')
- %td.line_content.parallel.noteable_line{class: [left[:type], left[:line_code], ('empty-cell' if left[:text].empty?)], data: { line_code: left[:line_code], position: left[:position].to_json }}= diff_line_content(left[:text])
+ %td.old_line.diff-line-num.empty-cell
+ %td.line_content.parallel
- - if right[:type] == 'new'
- - new_line_class = 'new'
- - new_line_code = right[:line_code]
- - new_position = right[:position]
+ - if right
+ - if right.meta?
+ = diff_match_line nil, right.new_pos, text: left.text, view: :parallel
- else
- - new_line_class = nil
- - new_line_code = left[:line_code]
- - new_position = left[:position]
-
- %td.new_line.diff-line-num{id: new_line_code, class: [new_line_class, ('empty-cell' unless right[:number])], data: { linenumber: right[:number] }}
- = link_to raw(right[:number]), "##{new_line_code}", id: new_line_code
- - if !@diff_notes_disabled && can?(current_user, :create_note, @project)
- = link_to_new_diff_note(new_line_code, new_position, 'new')
- %td.line_content.parallel.noteable_line{class: [new_line_class, new_line_code, ('empty-cell' if right[:text].empty?)], data: { line_code: new_line_code, position: new_position.to_json }}= diff_line_content(right[:text])
+ - right_line_code = diff_file.line_code(right)
+ - right_position = diff_file.position(right)
+ %td.new_line.diff-line-num{id: right_line_code, class: right.type, data: { linenumber: right.new_pos }}
+ %a{href: "##{right_line_code}" }= raw(right.new_pos)
+ %td.line_content.parallel.noteable_line{class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new')}= diff_line_content(right.text)
+ - else
+ %td.old_line.diff-line-num.empty-cell
+ %td.line_content.parallel
- unless @diff_notes_disabled
- - notes_left, notes_right = organize_comments(left, right)
- - if notes_left.present? || notes_right.present?
- = render "projects/notes/diff_notes_with_reply_parallel", notes_left: notes_left, notes_right: notes_right
+ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
+ - if discussion_left || discussion_right
+ = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+ - if !diff_file.new_file && last_line > 0
+ = diff_match_line last_line, last_line, bottom: true, view: :parallel
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index ea2a3e01277..e751dabdf99 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,7 +2,7 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
- %strong #{pluralize(diff_files.count, "changed file")}
+ %strong #{pluralize(diff_files.size, "changed file")}
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
and
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index 192093d1273..f1d2d4bf268 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -3,18 +3,14 @@
.suppressed-container
%a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show.
-%table.text-file.code.js-syntax-highlight{ class: too_big ? 'hide' : '' }
+%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- last_line = 0
- - diff_file.highlighted_diff_lines.each do |line|
- - last_line = line.new_pos
- = render "projects/diffs/line", line: line, diff_file: diff_file
+ - discussions = @grouped_diff_discussions unless @diff_notes_disabled
+ = render partial: "projects/diffs/line",
+ collection: diff_file.highlighted_diff_lines,
+ as: :line,
+ locals: { diff_file: diff_file, discussions: discussions }
- - unless @diff_notes_disabled
- - line_code = diff_file.line_code(line)
- - diff_notes = @grouped_diff_notes[line_code] if line_code
- - if diff_notes
- = render "projects/notes/diff_notes_with_reply", notes: diff_notes
-
- - if last_line > 0
- = render "projects/diffs/match_line", { line: "",
- line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file }
+ - last_line = diff_file.highlighted_diff_lines.last.new_pos
+ - if !diff_file.new_file && last_line > 0
+ = diff_match_line last_line, last_line, bottom: true
diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index 15536c17f8e..295a1b62535 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -2,9 +2,6 @@
%h4
Too many changes to show.
.pull-right
- - unless diff_hard_limit_enabled?
- = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm"
-
- if current_controller?(:commit) or current_controller?(:merge_requests)
- if current_controller?(:commit)
= link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-sm"
@@ -14,5 +11,5 @@
= link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-sm"
%p
To preserve performance only
- %strong #{diff_files.count} of #{diff_files.real_size}
+ %strong #{diff_files.size} of #{diff_files.real_size}
files are displayed.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 27a94fe02dc..b282aa52b25 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -4,6 +4,7 @@
%h4.prepend-top-0
Project settings
.col-lg-9
+ .project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset.append-bottom-0
.form-group
@@ -23,7 +24,7 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
- = link_to "(?)", help_page_path("public_access", "public_access")
+ = link_to "(?)", help_page_path("public_access/public_access")
- if can_change_visibility_level?(@project, current_user)
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: @project.visibility_level, form_model: @project)
- else
@@ -32,6 +33,10 @@
%strong
= visibility_level_label(@project.visibility_level)
.light= visibility_level_description(@project.visibility_level, @project)
+
+ .form-group
+ = render 'shared/allow_request_access', form: f
+
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
@@ -86,8 +91,6 @@
%hr
= render 'merge_request_settings', f: f
%hr
- = render 'builds_settings', f: f
- %hr
%fieldset.features.append-bottom-default
%h5.prepend-top-0
Project avatar
@@ -188,6 +191,7 @@
%h4.prepend-top-0.warning-title
Rename repository
.col-lg-9
+ = render 'projects/errors'
= form_for([@project.namespace.becomes(Namespace), @project]) do |f|
.form-group.project_name_holder
= f.label :name, class: 'label-light' do
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index eafa246d05f..36a6162a5a8 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -2,8 +2,12 @@
%tr.environment
%td
- %strong
- = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+ = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
+
+ %td
+ - if last_deployment
+ = user_avatar(user: last_deployment.user, size: 20)
+ %strong ##{last_deployment.id}
%td
- if last_deployment
@@ -15,3 +19,6 @@
%td
- if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)}
+
+ %td
+ = render 'projects/deployments/actions', deployment: last_deployment
diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml
index c07f4bd510c..6d040f5cfe6 100644
--- a/app/views/projects/environments/_form.html.haml
+++ b/app/views/projects/environments/_form.html.haml
@@ -1,7 +1,22 @@
-= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f|
- = form_errors(@environment)
- .form-group
- = f.label :name, 'Name', class: 'label-light'
- = f.text_field :name, required: true, class: 'form-control'
- = f.submit 'Create environment', class: 'btn btn-create'
- = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel'
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Environments
+ %p
+ Environments allow you to track deployments of your application
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci/environments")
+
+ = form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f|
+ = form_errors(@environment)
+
+ .form-group
+ = f.label :name, 'Name', class: 'label-light'
+ = f.text_field :name, required: true, class: 'form-control'
+ .form-group
+ = f.label :external_url, 'External URL', class: 'label-light'
+ = f.url_field :external_url, class: 'form-control'
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save'
+ = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml
new file mode 100644
index 00000000000..6d1bdb9320f
--- /dev/null
+++ b/app/views/projects/environments/edit.html.haml
@@ -0,0 +1,6 @@
+- page_title "Edit", @environment.name, "Environments"
+
+%h3.page-title
+ Edit environment
+%hr
+= render 'form'
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 5242021243e..b3eb5b0011a 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -17,15 +17,17 @@
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")
+ = 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.environments
+ %table.table.builds.environments
%tbody
%th Environment
- %th Last deployment
- %th Date
+ %th Last Deployment
+ %th Commit
+ %th
+ %th
= render @environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index da325efecd2..e51667ade2d 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -1,12 +1,6 @@
- page_title 'New Environment'
-.row.prepend-top-default.append-bottom-default
- .col-lg-3
- %h4.prepend-top-0
- New Environment
- %p
- Environments allow you to track deployments of your application
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci", "environments")
-
- = render 'form'
+%h3.page-title
+ New environment
+%hr
+= render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 53c62ef234d..8f8c1c4ce22 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -5,11 +5,11 @@
%div{ class: container_class }
.top-area
.col-md-9
- %h3.page-title= @environment.name.titleize
-
+ %h3.page-title= @environment.name.capitalize
.col-md-3
.nav-controls
- if can?(current_user, :update_environment, @environment)
+ = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
= link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
- if @deployments.blank?
@@ -20,16 +20,16 @@
Define environments in the deploy stage(s) in
%code .gitlab-ci.yml
to track deployments here.
- = link_to "Read more", help_page_path("ci", "environments"), class: "btn btn-success"
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- else
.table-holder
- %table.table.environments
+ %table.table.builds.environments
%thead
%tr
%th ID
%th Commit
%th Build
- %th Date
+ %th
%th
= render @deployments
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index dbe9ddfde2f..a1d79bdabda 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -31,11 +31,11 @@
- if current_user && can?(current_user, :fork_project, @project)
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
- = icon('code-fork fw')
+ = custom_icon('icon_fork')
Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
- = icon('code-fork fw')
+ = custom_icon('icon_fork')
Fork
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 73a7fc0e1ac..5242bc72b71 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -1,45 +1,54 @@
- page_title "Fork project"
-- if @namespaces.present?
- %h3.page-title Fork project
- %p.lead
- Click to fork the project to a user or group
- %hr
- .fork-namespaces
- - @namespaces.in_groups_of(6, false) do |group|
- .row
- - group.each do |namespace|
- .col-md-2.col-sm-3
- - if fork = namespace.find_fork_of(@project)
- .fork-thumbnail
- = link_to project_path(fork), title: "Visit project fork", class: 'has-tooltip' do
- = image_tag namespace_icon(namespace, 100)
- .caption
- %strong
- = namespace.human_name
- %div.text-primary
- Already forked
-
- - else
- .fork-thumbnail
- = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has-tooltip' do
- = image_tag namespace_icon(namespace, 100)
- .caption
- %strong
- = namespace.human_name
-
- %p.light
- Fork is a copy of a project repository.
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ Fork project
+ %p
+ A fork is a copy of a project.
%br
- Forking a repository allows you to do changes without affecting the original project.
-- else
- %h3 No available namespaces to fork the project
- %p.slead
- You must have permission to create a project in a namespace before forking.
+ Forking a repository allows you to make changes without affecting the original project.
+ .col-lg-9
+ .fork-namespaces
+ - if @namespaces.present?
+ %label.label-light
+ %span
+ Click to fork the project to a user or group
+ - @namespaces.in_groups_of(6, false) do |group|
+ .row
+ - group.each do |namespace|
+ - avatar = namespace_icon(namespace, 100)
+ - if fork = namespace.find_fork_of(@project)
+ .fork-thumbnail.forked
+ = link_to project_path(fork) do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ .no-avatar
+ = icon 'question'
+ - else
+ = image_tag avatar
+ .caption
+ = namespace.human_name
+ - else
+ .fork-thumbnail
+ = link_to namespace_project_forks_path(@project.namespace, @project, namespace_key: namespace.id), method: "POST" do
+ - if /no_((\w*)_)*avatar/.match(avatar)
+ .no-avatar
+ = icon 'question'
+ - else
+ = image_tag avatar
+ .caption
+ = namespace.human_name
+ - else
+ %label.label-light
+ %span
+ No available namespaces to fork the project.
+ %br
+ %small
+ You must have permission to create a project in a namespace before forking.
-.save-project-loader.hide
- .center
- %h2
- %i.fa.fa-spinner.fa-spin
- Forking repository
- %p Please wait a moment, this page will automatically refresh when ready.
+ .save-project-loader.hide
+ .center
+ %h2
+ %i.fa.fa-spinner.fa-spin
+ Forking repository
+ %p Please wait a moment, this page will automatically refresh when ready.
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 5bc5c71283e..331dc1fcc29 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
@@ -50,10 +50,12 @@
%td.duration
- if generic_commit_status.duration
- #{duration_in_words(generic_commit_status.finished_at, generic_commit_status.started_at)}
+ = icon("clock-o")
+ = time_interval_in_words(generic_commit_status.duration)
%td.timestamp
- if generic_commit_status.finished_at
+ = icon("calendar")
%span #{time_ago_with_tooltip(generic_commit_status.finished_at)}
- if defined?(coverage) && coverage
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
new file mode 100644
index 00000000000..31d40f6ad03
--- /dev/null
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -0,0 +1,9 @@
+%li.build
+ .build-content
+ - if subject.target_url
+ - link_to subject.target_url do
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+ - else
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index ca347406dfe..a231d684559 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,16 +1,18 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/application.js')
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.builds_enabled?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
index c58223fd39e..195f18afc76 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -19,4 +19,9 @@
]
}
var ctx = $("#build_timesChart").get(0).getContext("2d");
- new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
+ var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8
+ }
+ new Chart(ctx).Bar(data, options);
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 8fca07114fa..1fbf6ca2c1c 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -48,4 +48,9 @@
]
}
var ctx = $("##{scope}Chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
+ var options = { scaleOverlay: true, responsive: true, maintainAspectRatio: false };
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8
+ }
+ new Chart(ctx).Line(data, options);
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 65db8af494d..7e34a89f9ae 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -59,6 +59,10 @@
var container = $(selector).parent();
var generateChart = function() {
selector.attr('width', $(container).width());
+ if (window.innerWidth < 768) {
+ // Scale fonts if window width lower than 768px (iPad portrait)
+ options.scaleFontSize = 8
+ }
return new Chart(ctx).Bar(data, options);
};
// enabling auto-resizing
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index a985b442b2d..ac5f792d140 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -32,7 +32,7 @@
:javascript
$.ajax({
type: "GET",
- url: location.href,
+ url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, format: :json)}",
dataType: "json",
success: function (data) {
var graph = new ContributorsStatGraph();
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28..ca700cb3a3b 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 8151187d499..3fcf1692e09 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index 403adb7426b..f88b33018d0 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,25 +1,32 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do
- %span
- Issues
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+ %span
+ Issues
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
- %span
- Merge Requests
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %span
- Labels
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %span
- Milestones
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
+
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones \ No newline at end of file
diff --git a/app/views/projects/issues/_issue_by_email.html.haml b/app/views/projects/issues/_issue_by_email.html.haml
new file mode 100644
index 00000000000..72669372497
--- /dev/null
+++ b/app/views/projects/issues/_issue_by_email.html.haml
@@ -0,0 +1,27 @@
+.issues-footer.text-center
+ %button.issue-email-modal-btn{ type: "button", data: { toggle: "modal", target: "#issue-email-modal" } }
+ Email a new issue to this project
+
+#issue-email-modal.modal.fade{ tabindex: "-1", role: "dialog" }
+ .modal-dialog{ role: "document" }
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } }= icon("times")
+ %h4.modal-title
+ Create new issue by email
+ .modal-body
+ %p
+ Write an email to the below email address. (This is a private email address, so keep it secret.)
+ .email-modal-input-group.input-group
+ = text_field_tag :issue_email, email, class: "monospace js-select-on-focus form-control", readonly: true
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#issue_email')
+ %p
+ Send an email to this address to create an issue.
+ %p
+ Use the subject line as the title of your issue.
+ %p
+ Use the message as the body of your issue (feel free to include some nice
+ = succeed ")." do
+ = link_to "Markdown", help_page_path('markdown', 'markdown')
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index e93b7e0d66d..24749699c6d 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -2,7 +2,7 @@
.pull-right
#new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
= link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid),
- method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
+ method: :post, class: 'btn btn-new btn-inverted has-tooltip', title: @issue.to_branch_name, disabled: 'disabled' do
.checking
= icon('spinner spin')
Checking branches
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index c6fc499a7b8..6ea9f612d13 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -4,8 +4,8 @@
%ul.unstyled-list
- @related_branches.each do |branch|
%li
- - sha = @project.repository.find_branch(branch).target
- - pipeline = @project.pipeline(sha, branch) if sha
+ - target = @project.repository.find_branch(branch).target
+ - pipeline = @project.pipeline(target.sha, branch) if target
- if pipeline
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 312bd86ed04..1a87045aa60 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
- page_title "Issues"
+- new_issue_email = @project.new_issue_address(current_user)
= render "projects/issues/head"
= content_for :meta_tags do
@@ -18,12 +19,20 @@
Subscribe
= render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
+ = link_to new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New Issue",
+ id: "new_issue_link" do
New Issue
= render 'shared/issuable/filter', type: :issues
.issues-holder
- = render "issues"
+ = 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
@@ -32,7 +41,7 @@
Code, test, and deploy together
.blank-state
.blank-state-icon
- = navbar_icon("issues", size: 50)
+ = custom_icon("issues", size: 50)
%h3.blank-state-title
You don't have any issues right now.
%p.blank-state-text
@@ -40,3 +49,5 @@
- 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
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9b6a97c0959..3fb4191c60e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,7 +22,7 @@
- if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
.issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
@@ -37,14 +37,19 @@
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+ - if @issue.submittable_as_spam? && current_user.admin?
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
- if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
New issue
- if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
- Edit
+ - if @issue.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 53dd300c35c..d070979bcfe 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -4,5 +4,8 @@
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
+ %comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
+ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } }
+ {{ buttonText }}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index a5e67b95727..00bd4e143df 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -20,7 +20,7 @@
.mr-compare.merge-request
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
- = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
+ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
Commits
%span.badge= @commits.size
- if @pipeline
@@ -42,7 +42,7 @@
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown.
- else
- = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false
+ = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
- if @pipeline
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
@@ -52,11 +52,8 @@
$('#merge_request_assignee_id').val("#{current_user.id}").trigger("change");
e.preventDefault();
});
-
:javascript
- var merge_request
- merge_request = new MergeRequest({
- action: 'new',
- diffs_loaded: true,
- commits_loaded: true
+ var merge_request = new MergeRequest({
+ action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
+ setUrl: false
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 873ed9b59ee..9d8b4cc56be 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,8 +1,10 @@
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
-- if diff_view == 'parallel'
+- if diff_view == :parallel
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)}
@@ -14,6 +16,9 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
+ - if koding_enabled? && @repository.koding_yml
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
@@ -45,24 +50,38 @@
- if @commits_count.nonzero?
%ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
+ = 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
%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
+ = 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
+ %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= @merge_request.all_pipelines.size
%li.builds-tab
- = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do
+ = 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
%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
+ = 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"
- .tab-content
+ .tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
.content-block.content-block-small.oneline-block
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
@@ -76,6 +95,8 @@
- # This tab is always loaded via AJAX
#builds.builds.tab-pane
- # This tab is always loaded via AJAX
+ #pipelines.pipelines.tab-pane
+ - # This tab is always loaded via AJAX
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
new file mode 100644
index 00000000000..a524936f73c
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -0,0 +1,29 @@
+- class_bindings = "{ |
+ 'head': line.isHead, |
+ 'origin': line.isOrigin, |
+ 'match': line.hasMatch, |
+ 'selected': line.isSelected, |
+ 'unselected': line.isUnselected }"
+
+- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+= render "projects/merge_requests/show/mr_title"
+
+.merge-request-details.issuable-details
+ = render "projects/merge_requests/show/mr_box"
+
+= render 'shared/issuable/sidebar', issuable: @merge_request
+
+#conflicts{"v-cloak" => "true", data: { conflicts_path: conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request, format: :json),
+ resolve_conflicts_path: resolve_conflicts_namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request) } }
+ .loading{"v-if" => "isLoading"}
+ %i.fa.fa-spinner.fa-spin
+
+ .nothing-here-block{"v-if" => "hasError"}
+ {{conflictsData.errorMessage}}
+
+ = render partial: "projects/merge_requests/conflicts/commit_stats"
+
+ .files-wrapper{"v-if" => "!isLoading && !hasError"}
+ = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
+ = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
+ = render partial: "projects/merge_requests/conflicts/submit_form"
diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
new file mode 100644
index 00000000000..457c467fba9
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml
@@ -0,0 +1,20 @@
+.content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"}
+ .inline-parallel-buttons
+ .btn-group
+ %a.btn{ |
+ ":class" => "{'active': !isParallel}", |
+ "@click" => "handleViewTypeChange('inline')"}
+ Inline
+ %a.btn{ |
+ ":class" => "{'active': isParallel}", |
+ "@click" => "handleViewTypeChange('parallel')"}
+ Side-by-side
+
+ .js-toggle-container
+ .commit-stat-summary
+ Showing
+ %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}}
+ between
+ %strong {{conflictsData.source_branch}}
+ and
+ %strong {{conflictsData.target_branch}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
new file mode 100644
index 00000000000..19c7da4b5e3
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
@@ -0,0 +1,28 @@
+.files{"v-show" => "!isParallel"}
+ .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ .file-actions
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
+
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight
+ %table
+ %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
+ %template{"v-if" => "!line.isHeader"}
+ %td.diff-line-num.new_line{":class" => class_bindings}
+ %a {{line.new_line}}
+ %td.diff-line-num.old_line{":class" => class_bindings}
+ %a {{line.old_line}}
+ %td.line_content{":class" => class_bindings}
+ {{{line.richText}}}
+
+ %template{"v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.line_content.header{":class" => class_bindings}
+ %strong {{{line.richText}}}
+ %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ {{line.buttonTitle}}
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
new file mode 100644
index 00000000000..2e6f67c2eaf
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
@@ -0,0 +1,27 @@
+.files{"v-show" => "isParallel"}
+ .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"}
+ .file-title
+ %i.fa.fa-fw{":class" => "file.iconClass"}
+ %strong {{file.filePath}}
+ .file-actions
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
+
+ .diff-content.diff-wrap-lines
+ .diff-wrap-lines.code.file-content.js-syntax-highlight
+ %table
+ %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
+ %template{"v-for" => "line in section"}
+
+ %template{"v-if" => "line.isHeader"}
+ %td.diff-line-num.header{":class" => class_bindings}
+ %td.line_content.header{":class" => class_bindings}
+ %strong {{line.richText}}
+ %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ {{line.buttonTitle}}
+
+ %template{"v-if" => "!line.isHeader"}
+ %td.diff-line-num.old_line{":class" => class_bindings}
+ {{line.lineNumber}}
+ %td.line_content.parallel{":class" => class_bindings}
+ {{{line.richText}}}
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
new file mode 100644
index 00000000000..78bd4133ea2
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -0,0 +1,15 @@
+.content-block.oneline-block.files-changed
+ %strong.resolved-count {{resolvedCount}}
+ of
+ %strong.total-count {{conflictsCount}}
+ conflicts have been resolved
+
+ .commit-message-container.form-group
+ .max-width-marker
+ %textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
+ {{{conflictsData.commitMessage}}}
+
+ %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
+ %span {{commitButtonText}}
+
+ = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
index 81de60f116c..808ef7fed27 100644
--- a/app/views/projects/merge_requests/show/_builds.html.haml
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -1,2 +1 @@
= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
-
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 1b0bae86ad4..99c71e1454a 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,6 +1,6 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options),
- project: @merge_request.project, diff_refs: @merge_request.diff_refs
+ = render 'projects/merge_requests/show/versions'
+ = render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index b3bea900d42..b727efaa6a6 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -8,7 +8,7 @@
%p
%strong Step 1.
Fetch and check out the branch for this merge request
- = clipboard_button_with_class({clipboard_target: "pre#merge-info-1"}, css_class: "btn-clipboard")
+ = clipboard_button(clipboard_target: "pre#merge-info-1")
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
@@ -25,7 +25,7 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- = clipboard_button_with_class({clipboard_target: "pre#merge-info-3"}, css_class: "btn-clipboard")
+ = clipboard_button(clipboard_target: "pre#merge-info-3")
%pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
@@ -38,7 +38,7 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- = clipboard_button_with_class({clipboard_target: "pre#merge-info-4"}, css_class: "btn-clipboard")
+ = clipboard_button(clipboard_target: "pre#merge-info-4")
%pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 5bf5210aeab..098ce19da21 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -14,18 +14,18 @@
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- %li{ class: issue_button_visibility(@merge_request, true) }
+ %li{ class: merge_request_button_visibility(@merge_request, true) }
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- %li{ class: issue_button_visibility(@merge_request, false) }
+ %li{ class: merge_request_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request'
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
= link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do
Edit
diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml
new file mode 100644
index 00000000000..afe3f3430c6
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_pipelines.html.haml
@@ -0,0 +1 @@
+= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
new file mode 100644
index 00000000000..2da70ce7137
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -0,0 +1,31 @@
+- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+
+- if merge_request_diffs.size > 1
+ .mr-version-switch
+ Version:
+ %span.dropdown.inline
+ %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
+ %strong.monospace<
+ - if @merge_request_diff.latest?
+ #{"latest"}
+ - else
+ #{@merge_request_diff.head_commit.short_id}
+ %span.caret
+ %ul.dropdown-menu.dropdown-menu-selectable
+ - merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong.monospace
+ #{merge_request_diff.head_commit.short_id}
+ %br
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+
+ - unless @merge_request_diff.latest?
+ %span.prepend-left-default
+ = icon('info-circle')
+ This version is not the latest one. Comments are disabled
+ .pull-right
+ %span.monospace
+ #{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id}
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 489c632ae22..494695a03a5 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,6 +1,6 @@
- if @pipeline
.mr-widget-heading
- - %w[success skipped canceled failed running pending].each do |status|
+ - %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_icon_for_status(status)
%span
@@ -42,3 +42,16 @@
.ci_widget.ci-error{style: "display:none"}
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
+
+- @merge_request.environments.each do |environment|
+ .mr-widget-heading
+ .ci_widget.ci-success
+ = ci_icon_for_status("success")
+ %span.hidden-sm
+ Deployed to
+ = succeed '.' do
+ = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment'
+ - external_url = environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank' do
+ = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 19b5d0ff066..7794d6d7df2 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -6,7 +6,7 @@
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ - if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
%p
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index dc18f715f25..6f5ee5f16c5 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -1,6 +1,12 @@
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
+ -# After conflicts are resolved, the user is redirected back to the MR page.
+ -# There is a short window before background workers run and GitLab processes
+ -# the new push and commits, during which it will think the conflicts still exist.
+ -# We send this param to get the widget to treat the MR as having no more conflicts.
+ - resolved_conflicts = params[:resolved_conflicts]
+
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
- elsif @merge_request.commits.blank?
@@ -9,7 +15,7 @@
= render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
- - elsif @merge_request.cannot_be_merged?
+ - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
@@ -19,7 +25,7 @@
= render 'projects/merge_requests/widget/open/not_allowed'
- elsif !@merge_request.mergeable_ci_state? && @pipeline && @pipeline.failed?
= render 'projects/merge_requests/widget/open/build_failed'
- - elsif @merge_request.can_be_merged?
+ - elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- if mr_closes_issues.present?
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index d9efe81701f..ea618263a4a 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -23,7 +23,8 @@
preparing: "{{status}} build",
normal: "Build {{status}}"
},
- builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
+ builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
diff --git a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
index 06ab0a3fa00..af3096f04d9 100644
--- a/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_conflicts.html.haml
@@ -3,8 +3,19 @@
This merge request contains merge conflicts
%p
- Please resolve these conflicts or
- - if @merge_request.can_be_merged_by?(current_user)
+ Please
+ - if @merge_request.conflicts_can_be_resolved_by?(current_user)
+ - if @merge_request.conflicts_can_be_resolved_in_ui?
+ = link_to "resolve these conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ - else
+ %span.has-tooltip{title: "These conflicts cannot be resolved through GitLab"}
+ resolve these conflicts locally
+ - else
+ resolve these conflicts
+
+ or
+
+ - if @merge_request.can_be_merged_via_command_line_by?(current_user)
#{link_to "merge this request manually", "#modal_merge_info", class: "how_to_merge_link vlink", "data-toggle" => "modal"}.
- else
ask someone with write access to this repository to merge this request manually.
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 091af4df4a1..b2ece44d966 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,7 @@
- page_title "Network", @ref
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/raphael.js')
- = page_specific_javascript_tag('network/application.js')
+ = page_specific_javascript_tag('network/network_bundle.js')
= render "projects/commits/head"
= render "head"
%div{ class: container_class }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 05f33b78a47..0a1e2bb2cc6 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -46,54 +46,35 @@
%div
- if github_import_enabled?
= link_to new_import_github_path, class: 'btn import_github' do
- = icon 'github', text: 'GitHub'
+ = icon('github', text: 'GitHub')
%div
- if bitbucket_import_enabled?
- - if bitbucket_import_configured?
- = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do
- %i.fa.fa-bitbucket
- Bitbucket
- - else
- = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do
- %i.fa.fa-bitbucket
- Bitbucket
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
- - if gitlab_import_configured?
- = link_to status_import_gitlab_path, class: 'btn import_gitlab' do
- %i.fa.fa-heart
- GitLab.com
- - else
- = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do
- %i.fa.fa-heart
- GitLab.com
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- - if gitorious_import_enabled?
- = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
- %i.icon-gitorious.icon-gitorious-small
- Gitorious.org
- %div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
- %i.fa.fa-google
- Google Code
+ = icon('google', text: 'Google Code')
%div
- if fogbugz_import_enabled?
= link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- %i.fa.fa-bug
- Fogbugz
+ = icon('bug', text: 'Fogbugz')
%div
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
- %i.fa.fa-git
- %span Repo by URL
- %div
- - if gitlab_project_import_enabled?
- = link_to new_import_gitlab_project_path, class: 'btn import_gitlab_project project-submit' do
- %i.fa.fa-gitlab
- %span GitLab export
+ = icon('git', text: 'Repo by URL')
+ %div{ class: 'import_gitlab_project' }
+ - if gitlab_project_import_enabled? && current_user.is_admin?
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
.js-toggle-content.hide
= render "shared/import_form", f: f
@@ -107,7 +88,7 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
- = link_to "(?)", help_page_path("public_access", "public_access")
+ = 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)
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
@@ -130,28 +111,33 @@
$(".modal").hide();
});
- $('.import_gitlab_project').bind('click', function() {
- var _href = $("a.import_gitlab_project").attr("href");
- $(".import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
+ $('.btn_import_gitlab_project').bind('click', function() {
+ var _href = $("a.btn_import_gitlab_project").attr("href");
+ $(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
- $('.import_gitlab_project').attr('disabled',true)
- $('.import_gitlab_project').attr('title', 'Project path required.');
+ $('.btn_import_gitlab_project').attr('disabled',true)
+ $('.import_gitlab_project').attr('title', 'Project path and name required.');
$('.import_gitlab_project').click(function( event ) {
- if($('.import_gitlab_project').attr('disabled')) {
+ if($('.btn_import_gitlab_project').attr('disabled')) {
event.preventDefault();
- new Flash("Please enter a path for the project to be imported to.");
+ new Flash("Please enter path and name for the project to be imported to.");
}
});
$('#project_path').keyup(function(){
if($(this).val().length !=0) {
- $('.import_gitlab_project').attr('disabled', false);
+ $('.btn_import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title','');
$(".flash-container").html("")
} else {
- $('.import_gitlab_project').attr('disabled',true);
- $('.import_gitlab_project').attr('title', 'Project path required.');
+ $('.btn_import_gitlab_project').attr('disabled',true);
+ $('.import_gitlab_project').attr('title', 'Project path and name required.');
}
- })
+ });
+
+ $('.import_git').click(function( event ) {
+ $projectImportUrl = $('#project_import_url')
+ $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
+ });
diff --git a/app/views/projects/notes/_diff_notes_with_reply.html.haml b/app/views/projects/notes/_diff_notes_with_reply.html.haml
deleted file mode 100644
index ec6c4938efc..00000000000
--- a/app/views/projects/notes/_diff_notes_with_reply.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- note = notes.first
-%tr.notes_holder
- %td.notes_line{ colspan: 2 }
- %td.notes_content
- %ul.notes{ data: { discussion_id: note.discussion_id } }
- = render partial: "projects/notes/note", collection: notes, as: :note
- = link_to_reply_discussion(note)
diff --git a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml b/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
deleted file mode 100644
index e50a4f86d03..00000000000
--- a/app/views/projects/notes/_diff_notes_with_reply_parallel.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- note_left = notes_left.present? ? notes_left.first : nil
-- note_right = notes_right.present? ? notes_right.first : nil
-
-%tr.notes_holder
- - if note_left
- %td.notes_line.old
- %td.notes_content.parallel.old
- %ul.notes{ data: { discussion_id: note_left.discussion_id } }
- = render partial: "projects/notes/note", collection: notes_left, as: :note
-
- = link_to_reply_discussion(note_left, 'old')
- - else
- %td.notes_line.old= ""
- %td.notes_content.parallel.old= ""
-
- - if note_right
- %td.notes_line.new
- %td.notes_content.parallel.new
- %ul.notes{ data: { discussion_id: note_right.discussion_id } }
- = render partial: "projects/notes/note", collection: notes_right, as: :note
-
- = link_to_reply_discussion(note_right, 'new')
- - else
- %td.notes_line.new= ""
- %td.notes_content.parallel.new= ""
diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml
deleted file mode 100644
index 7869d6413d8..00000000000
--- a/app/views/projects/notes/_discussion.html.haml
+++ /dev/null
@@ -1,46 +0,0 @@
-- note = discussion_notes.first
-- expanded = !note.diff_note? || note.active?
-%li.note.note-discussion.timeline-entry
- .timeline-entry-inner
- .timeline-icon
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author), class: "avatar s40"
- .timeline-content
- .discussion.js-toggle-container{ class: note.discussion_id }
- .discussion-header
- = link_to_member(@project, note.author, avatar: false)
-
- .inline.discussion-headline-light
- = note.author.to_reference
- started a discussion on
-
- - if note.for_commit?
- - commit = note.noteable
- - if commit
- commit
- = link_to commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code), class: 'monospace'
- - else
- a deleted commit
- - else
- - if note.active?
- = link_to diffs_namespace_project_merge_request_path(note.project.namespace, note.project, note.noteable, anchor: note.line_code) do
- the diff
- - else
- an outdated diff
-
- = time_ago_with_tooltip(note.created_at, placement: "bottom", html_class: "note-created-ago")
-
- .discussion-actions
- = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
- - if expanded
- = icon("chevron-up")
- - else
- = icon("chevron-down")
-
- Toggle discussion
-
- .discussion-body.js-toggle-content{ class: ("hide" unless expanded) }
- - if note.diff_note?
- = render "projects/notes/discussions/diff_with_notes", discussion_notes: discussion_notes
- - else
- = render "projects/notes/discussions/notes", discussion_notes: discussion_notes
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 7c61ba750fe..402f5b52f5e 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form" }, authenticity_token: true do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new-note js-new-note-form js-quick-submit common-note-form", "data-noteable-iid" => @note.noteable.try(:iid), }, authenticity_token: true do |f|
= hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
@@ -10,8 +10,12 @@
= f.hidden_field :position
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
- = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', placeholder: "Write a comment or drag your files here..."
- = render 'projects/notes/hints'
+ = render 'projects/zen', f: f,
+ attr: :note,
+ classes: 'note-textarea js-note-text',
+ placeholder: "Write a comment or drag your files here...",
+ supports_slash_commands: true
+ = render 'projects/notes/hints', supports_slash_commands: true
.error-alert
.note-form-actions.clearfix
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 7d1cbc62e86..6c14f48d41b 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -1,8 +1,15 @@
+- supports_slash_commands = local_assigns.fetch(:supports_slash_commands, false)
.comment-toolbar.clearfix
.toolbar-text
Styling with
- = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1
- is supported
+ = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
+ - if supports_slash_commands
+ and
+ = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
+ are
+ - else
+ is
+ supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file \ No newline at end of file
+ Attach a file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index af0046886fb..7c82177f9ea 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,5 +1,6 @@
- return unless note.author
- return if note.cross_reference_not_visible_for?(current_user)
+- can_resolve = can?(current_user, :resolve_note, note)
- note_editable = note_editable?(note)
%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
@@ -16,21 +17,50 @@
commented
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- .note-actions
- - access = note_max_access_for_user(note)
- - if access and not note.system
- %span.note-role.hidden-xs= access
- - if current_user and not note.system
- = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
- = icon('spinner spin')
- = icon('smile-o')
- - if note_editable
- = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
- = icon('pencil')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
- = icon('trash-o')
+ - unless note.system?
+ .note-actions
+ - access = note_max_access_for_user(note)
+ - if access
+ %span.note-role.hidden-xs= access
+
+ - if note.resolvable?
+ %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
+ ":project-path" => "'#{note.project.path}'",
+ ":discussion-id" => "'#{note.discussion_id}'",
+ ":note-id" => note.id,
+ ":resolved" => note.resolved?,
+ ":can-resolve" => can_resolve,
+ ":resolved-by" => "'#{note.resolved_by.try(:name)}'",
+ "v-show" => "#{can_resolve || note.resolved?}",
+ "inline-template" => true,
+ "v-ref:note_#{note.id}" => true }
+
+ .note-action-button
+ = icon("spin spinner", "v-show" => "loading")
+ %button.line-resolve-btn{ type: "button",
+ class: ("is-disabled" unless can_resolve),
+ ":class" => "{ 'is-active': isResolved }",
+ ":aria-label" => "buttonText",
+ "@click" => "resolve",
+ ":title" => "buttonText",
+ "v-show" => "!loading",
+ "v-el:button" => true }
+
+ = render "shared/icons/icon_status_success.svg"
+
+ - if current_user
+ - if note.emoji_awardable?
+ = link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
+ = icon('spinner spin')
+ = icon('smile-o', class: 'link-highlight')
+
+ - if note_editable
+ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
+ = icon('pencil', class: 'link-highlight')
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
+ = icon('trash-o')
.note-body{class: note_editable ? 'js-task-list-container' : ''}
- .note-text
+ .note-text.md
= preserve do
= note.note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index ebf7e8a9cb3..022578bd6db 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -1,10 +1,8 @@
- if @discussions.present?
- - @discussions.each do |discussion_notes|
- - note = discussion_notes.first
- - if note_for_main_target?(note)
- = render partial: "projects/notes/note", object: note, as: :note
+ - @discussions.each do |discussion|
+ - if discussion.for_target?(@noteable)
+ = render partial: "projects/notes/note", object: discussion.first_note, as: :note
- else
- = render 'projects/notes/discussion', discussion_notes: discussion_notes
+ = render 'discussions/discussion', discussion: discussion
- else
- - @notes.each do |note|
- = render partial: "projects/notes/note", object: note, as: :note
+ = render partial: "projects/notes/note", collection: @notes, as: :note
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 1c39ce897a3..74538a9723e 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -2,6 +2,8 @@
= render "projects/notes/notes"
%ul.notes.notes-form.timeline
%li.timeline-entry
+ .flash-container.timeline-content
+
- if can? current_user, :create_note, @project
.timeline-icon.hidden-xs.hidden-sm
%a.author_link{ href: user_path(current_user) }
@@ -12,9 +14,9 @@
.disabled-comment.text-center
.disabled-comment-text.inline
Please
- = link_to "register",new_user_session_path
+ = link_to "register", new_session_path(:user, redirect_to_referer: 'yes')
or
- = link_to "login",new_user_session_path
+ = link_to "login", new_session_path(:user, redirect_to_referer: 'yes')
to post a comment
:javascript
diff --git a/app/views/projects/notes/discussions/_diff_with_notes.html.haml b/app/views/projects/notes/discussions/_diff_with_notes.html.haml
deleted file mode 100644
index 4a69b8f8840..00000000000
--- a/app/views/projects/notes/discussions/_diff_with_notes.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- note = discussion_notes.first
-- diff_file = note.diff_file
-- return unless diff_file
-
-- blob = note.blob
-
-.diff-file.file-holder
- .file-title
- = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_file.content_commit, project: note.project, url: diff_note_path(note)
-
- .diff-content.code.js-syntax-highlight
- %table
- - note.truncated_diff_lines.each do |line|
- = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
-
- - if note.for_line?(line)
- = render "projects/notes/diff_notes_with_reply", notes: discussion_notes
diff --git a/app/views/projects/notes/discussions/_notes.html.haml b/app/views/projects/notes/discussions/_notes.html.haml
deleted file mode 100644
index a785149549d..00000000000
--- a/app/views/projects/notes/discussions/_notes.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- note = discussion_notes.first
-.panel.panel-default
- .notes{ data: { discussion_id: note.discussion_id } }
- %ul.notes.timeline
- = render partial: "projects/notes/note", collection: discussion_notes, as: :note
- = link_to_reply_discussion(note)
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index d65faf86d4e..f611ddc8f5f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,19 +1,21 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- %span
- Builds
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
- - if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde7..063e83a407a 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,7 @@
= 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
+ = time_interval_in_words(@pipeline.duration)
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 6a127afa410..5f466bdbac2 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -28,10 +28,10 @@
.nav-controls
- if can? current_user, :create_pipeline, @project
= link_to new_namespace_project_pipeline_path(@project.namespace, @project), class: 'btn btn-create' do
- New pipeline
+ Run pipeline
- unless @repository.gitlab_ci_yml
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+ = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
@@ -45,13 +45,13 @@
.table-holder
%table.table.builds
%tbody
- %th ID
+ %th Status
%th Commit
- stages.each do |stage|
%th.stage
%span.has-tooltip{ title: "#{stage.titleize}" }
= stage.titleize
- %th Duration
+ %th
%th
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 5f4ec2e40c8..55202725b9e 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -9,7 +9,7 @@
.form-group
= f.label :ref, 'Create for', class: 'control-label'
.col-sm-10
- = f.text_field :ref, required: true, tabindex: 2, class: 'form-control'
+ = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref
.help-block Existing branch name, tag
.form-actions
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
diff --git a/app/views/projects/pipelines_settings/_badge.html.haml b/app/views/projects/pipelines_settings/_badge.html.haml
new file mode 100644
index 00000000000..7b7fa56d993
--- /dev/null
+++ b/app/views/projects/pipelines_settings/_badge.html.haml
@@ -0,0 +1,27 @@
+.row{ class: badge.title.gsub(' ', '-') }
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = badge.title.capitalize
+ .col-lg-9
+ .prepend-top-10
+ .panel.panel-default
+ .panel-heading
+ %b
+ = badge.title.capitalize
+ &middot;
+ = badge.to_html
+ .pull-right
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
+ .panel-body
+ .row
+ .col-md-2.text-center
+ Markdown
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.md', badge.to_markdown)
+ .row
+ %hr
+ .row
+ .col-md-2.text-center
+ HTML
+ .col-md-10.code.js-syntax-highlight
+ = highlight('.html', badge.to_html)
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
new file mode 100644
index 00000000000..8c7222bfe3d
--- /dev/null
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -0,0 +1,80 @@
+- page_title "CI/CD Pipelines"
+
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ = page_title
+ .col-lg-9
+ %h5.prepend-top-0
+ Pipelines
+ = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project), remote: true, authenticity_token: true do |f|
+ %fieldset.builds-feature
+ - unless @repository.gitlab_ci_yml
+ .form-group
+ %p Pipelines need to be configured before you can begin using Continuous Integration.
+ = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ .form-group
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch_false do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch_true do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+ %li
+ gcovr (C/C++) -
+ %code ^TOTAL.*\s+(\d+\%)$
+ %li
+ tap --coverage-report=text-summary (Node.js) -
+ %code ^Statements\s*:\s*([^%]+)
+
+ .form-group
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public pipelines
+ .help-block Allow everyone to access pipelines for Public and Internal projects
+
+ .form-group.append-bottom-default
+ = f.label :runners_token, "Runners token", class: 'label-light'
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
+
+ = f.submit 'Save changes', class: "btn btn-save"
+
+%hr
+
+.row.prepend-top-default
+ = render partial: 'badge', collection: @badges
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 82892a33358..fa8cbf71733 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -12,7 +12,16 @@
= select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
+ %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this project.
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496..9d063b3081f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Members"
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef89060..833954bc039 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 97cb1a9052b..04b19a8c5a7 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -1,26 +1,28 @@
-%h5.prepend-top-0
- Already Protected (#{@protected_branches.size})
-- if @protected_branches.empty?
- %p.settings-message.text-center
- No branches are protected, protect a branch with the form above.
-- else
- - can_admin_project = can?(current_user, :admin_project, @project)
- .table-responsive
- %table.table.protected-branches-list
+.panel.panel-default.protected-branches-list
+ - if @protected_branches.empty?
+ .panel-heading
+ %h3.panel-title
+ Protected branch (#{@protected_branches.size})
+ %p.settings-message.text-center
+ There are currently no protected branches, protect a branch with the form above.
+ - else
+ - can_admin_project = can?(current_user, :admin_project, @project)
+
+ %table.table.table-bordered
%colgroup
- %col{ width: "30%" }
%col{ width: "25%" }
+ %col{ width: "30%" }
%col{ width: "25%" }
- - if can_admin_project
- %col
+ %col{ width: "20%" }
%thead
%tr
- %th Protected Branch
- %th Commit
- %th Developers Can Push
+ %th Protected branch (#{@protected_branches.size})
+ %th Last commit
+ %th Allowed to merge
+ %th Allowed to push
- if can_admin_project
%th
%tbody
= render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
- = paginate @protected_branches, theme: 'gitlab'
+ = paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
new file mode 100644
index 00000000000..e95a3b1b4c3
--- /dev/null
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -0,0 +1,41 @@
+= form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
+ .panel.panel-default
+ .panel-heading
+ %h3.panel-title
+ Protect a branch
+ .panel-body
+ .form-horizontal
+ = form_errors(@protected_branch)
+ .form-group
+ = f.label :name, class: 'col-md-2 text-right' do
+ Branch:
+ .col-md-10
+ = render partial: "dropdown", locals: { f: f }
+ .help-block
+ = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
+ such as
+ %code *-stable
+ or
+ %code production/*
+ are supported
+ .form-group
+ %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
+ Allowed to merge:
+ .col-md-10
+ .merge_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-merge wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
+ .form-group
+ %label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
+ Allowed to push:
+ .col-md-10
+ .push_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-push wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
+
+ .panel-footer
+ = f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
index b803d932e67..a9e27df5a87 100644
--- a/app/views/projects/protected_branches/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -1,17 +1,15 @@
= f.hidden_field(:name)
-= dropdown_tag("Protected Branch",
- options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit',
+= dropdown_tag('Select branch or create wildcard',
+ options: { toggle_class: 'js-protected-branch-select js-filter-submit wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_branch_name],
project_id: @project.try(:id) } }) do
- %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list
+ %ul.dropdown-footer-list
%li
= link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do
- Create new
-
-:javascript
- new ProtectedBranchSelect();
+ Create wildcard
+ %code
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 474aec3a97c..0628134b1bb 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -1,5 +1,4 @@
-- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch)
-%tr
+%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } }
%td
= protected_branch.name
- if @project.root_ref?(protected_branch.name)
@@ -14,8 +13,9 @@
= time_ago_with_tooltip(commit.committed_date)
- else
(branch was removed from repository)
- %td
- = check_box_tag("developers_can_push", protected_branch.id, protected_branch.developers_can_push, data: { url: url })
+
+ = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+
- if can_admin_project
%td
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right"
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
new file mode 100644
index 00000000000..d6044aacaec
--- /dev/null
+++ b/app/views/projects/protected_branches/_update_protected_branch.html.haml
@@ -0,0 +1,10 @@
+%td
+ = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_levels.first.access_level
+ = dropdown_tag( (protected_branch.merge_access_levels.first.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container',
+ data: { field_name: "allowed_to_merge_#{protected_branch.id}", access_level_id: protected_branch.merge_access_levels.first.id }})
+%td
+ = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_levels.first.access_level
+ = dropdown_tag( (protected_branch.push_access_levels.first.humanize || 'Select') ,
+ options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container',
+ data: { field_name: "allowed_to_push_#{protected_branch.id}", access_level_id: protected_branch.push_access_levels.first.id }})
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 5669713d9a1..49dcc9a6ba4 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -6,36 +6,15 @@
= page_title
%p Keep stable branches secure and force developers to use merge requests.
%p.prepend-top-20
- Protected branches are designed to:
+ By default, protected branches are designed to:
%ul
- %li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"}
- %li prevent anyone from force pushing to the branch
- %li prevent anyone from deleting the branch
- %p.append-bottom-0 Read more about #{link_to "project permissions", help_page_path("permissions", "permissions"), class: "underlined-link"}
+ %li prevent their creation, if not already created, from everybody except Masters
+ %li prevent pushes from everybody except Masters
+ %li prevent <strong>anyone</strong> from force pushing to the branch
+ %li prevent <strong>anyone</strong> from deleting the branch
+ %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
.col-lg-9
- %h5.prepend-top-0
- Protect a branch
- if can? current_user, :admin_project, @project
- = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f|
- = form_errors(@protected_branch)
+ = render 'create_protected_branch'
- .form-group
- = f.label :name, "Branch", class: "label-light"
- = render partial: "dropdown", locals: { f: f }
- %p.help-block
- = link_to "Wildcards", help_page_path(category: 'workflow', file: 'protected_branches', format: 'md', anchor: "wildcard-protected-branches")
- such as
- %code *-stable
- or
- %code production/*
- are supported.
-
- .form-group
- = f.check_box :developers_can_push, class: "pull-left"
- .prepend-left-20
- = f.label :developers_can_push, "Developers can push", class: "label-light append-bottom-0"
- %p.light.append-bottom-0
- Allow developers to push to this branch
- = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true
- %hr
= render "branches_list"
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 835398b6f98..33d5cbff420 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,18 +1,20 @@
+- @no_container = true
- page_title "Edit", @tag.name, "Tags"
= render "projects/commits/head"
-.row-content-block
- .oneline
- .title
- Release notes for tag
- %strong #{@tag.name}
+%div{ class: container_class }
+ .sub-header-block.no-bottom-space
+ .oneline
+ .title
+ Release notes for tag
+ %strong #{@tag.name}
+
-.prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= 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 your release notes or drag files here..."
= render 'projects/notes/hints'
.error-alert
- .form-actions.prepend-top-default
+ .prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 1f13ea28b4e..752fbc21a11 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -8,9 +8,10 @@
.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
+
= form.submit 'Save changes', class: 'btn btn-save'
&nbsp;
- if @service.valid? && @service.activated?
- disabled = @service.can_test? ? '':'disabled'
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}"
+ = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled}", title: @service.disabled_title
= link_to "Cancel", namespace_project_services_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 58d8e068754..340e159c874 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -43,6 +43,10 @@
%li
= link_to 'Contribution guide', contribution_guide_path(@project)
+ - if @repository.gitlab_ci_yml
+ %li
+ = link_to 'CI configuration', ci_configuration_path(@project)
+
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.missing
@@ -60,10 +64,12 @@
%li.missing
= 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
- if current_user
= render 'shared/members/access_request_buttons', source: @project
+ = render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render "projects/buttons/download"
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 6c994ae486b..1646bcf4b8a 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Snippets"
-.row-content-block.top-block
+.sub-header-block
.pull-right
- 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
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index b7d7d5c5382..395d7af6cbb 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -1,36 +1,39 @@
+- @no_container = true
- page_title @tag.name, "Tags"
= render "projects/commits/head"
-.row-content-block
- .pull-right
- - if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has-tooltip', title: 'Edit release notes' do
- = icon("pencil")
- = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse files' do
- = icon('files-o')
- = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has-tooltip', title: 'Browse commits' do
- = icon('history')
- - if can? current_user, :download_code, @project
- = render 'projects/tags/download', ref: @tag.name, project: @project
- - 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
- %i.fa.fa-trash-o
- .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)
+%div{ class: container_class }
+ .sub-header-block
+ .pull-right.tag-buttons
+ - 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")
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse files' do
+ = icon('files-o')
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
+ = icon('history')
+ - if can? current_user, :download_code, @project
+ = render 'projects/tags/download', ref: @tag.name, project: @project
+ - 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
+ %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)
-.append-bottom-default.prepend-top-default
- - if @release.description.present?
- .description
- .wiki
- = preserve do
- = markdown @release.description
- - else
- This tag has no release notes.
+ .append-bottom-default.prepend-top-default
+ - if @release.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown @release.description
+ - else
+ This tag has no release notes.
diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml
index a3a4bd4f752..84da16b6bb1 100644
--- a/app/views/projects/tree/_tree_commit_column.html.haml
+++ b/app/views/projects/tree/_tree_commit_column.html.haml
@@ -1,2 +1,2 @@
%span.str-truncated
- = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link"
+ = link_to_gfm commit.full_title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link"
diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml
new file mode 100644
index 00000000000..0a5c6f048f7
--- /dev/null
+++ b/app/views/projects/tree/_tree_row.html.haml
@@ -0,0 +1,6 @@
+- if tree_row.type == :tree
+ = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' }
+- elsif tree_row.type == :blob
+ = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' }
+- elsif tree_row.type == :commit
+ = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item'
diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml
index 7d9bd08385a..dcf1f767bf7 100644
--- a/app/views/projects/update.js.haml
+++ b/app/views/projects/update.js.haml
@@ -6,4 +6,4 @@
$(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide();
$('.project-edit-container').show();
- $('.project-edit-content .btn-save').enable();
+ $('.edit-project .btn-save').enable();
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 797a1a59e9f..643f7c589e6 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -18,9 +18,14 @@
.error-alert
.help-block
- To link to a (new) page, simply type
- %code [Link Title](page-slug)
- \.
+ = succeed '.' do
+ To link to a (new) page, simply type
+ %code [Link Title](page-slug)
+
+ = succeed '.' do
+ More examples are in the
+ = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown")
+
.form-group
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index f8ea479e0b1..551a20c1044 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,15 @@
-.nav-links.sub-nav
- %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)
+.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#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
+ = 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'
+ = render 'projects/wikis/new'
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index 8163aff43b6..e0400083870 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -1,6 +1,7 @@
- project = note.project
- note_url = Gitlab::UrlBuilder.build(note)
-- noteable_identifier = note.noteable.try(:iid) || note.noteable.id
+- noteable_identifier = note.noteable.try(:iid) || note.noteable.try(:id)
+
.search-result-row
%h5.note-search-caption.str-truncated
%i.fa.fa-comment
@@ -10,7 +11,10 @@
&middot;
- if note.for_commit?
- = link_to "Commit #{truncate_sha(note.commit_id)}", note_url
+ = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do
+ = truncate_sha(note.commit_id)
+ %span.light Commit deleted
+
- else
%span #{note.noteable_type.titleize} ##{noteable_identifier}
&middot;
diff --git a/app/views/shared/_allow_request_access.html.haml b/app/views/shared/_allow_request_access.html.haml
new file mode 100644
index 00000000000..53a99a736c0
--- /dev/null
+++ b/app/views/shared/_allow_request_access.html.haml
@@ -0,0 +1,6 @@
+.checkbox
+ = form.label :request_access_enabled do
+ = form.check_box :request_access_enabled
+ %strong Allow users to request access
+ %br
+ %span.descr Allow users to request access if visibility is public or internal.
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 627814bcfae..65a3a6bddab 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
= f.label :import_url, class: 'control-label' do
%span Git repository URL
.col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+ = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
.well.prepend-top-20
%ul
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
index dce492352ac..e324d0e5203 100644
--- a/app/views/shared/_labels_row.html.haml
+++ b/app/views/shared/_labels_row.html.haml
@@ -1,9 +1,5 @@
- labels.each do |label|
%span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" }
- = link_to label.name, label_filter_path(@project, label, type: controller.controller_name),
- class: "btn btn-transparent has-tooltip",
- style: "background-color: #{label.color};",
- title: escape_once(label.description),
- data: { container: "body" }
+ = link_to_label(label, css_class: 'btn btn-transparent')
%button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } }
= icon("times")
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index b07f1c5603e..9b67422da2c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,9 +1,9 @@
-<svg width="36" height="36" id="tanuki-logo">
- <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
- <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
- <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
- <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
- <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
- <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
- <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
+<svg width="36" height="36" class="tanuki-logo">
+ <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
+ <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
+ <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
+ <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+ <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+ <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+ <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
</svg>
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
new file mode 100644
index 00000000000..4e3b1b3a571
--- /dev/null
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -0,0 +1,4 @@
+.fade-left
+ = icon('angle-left')
+.fade-right
+ = icon('angle-right') \ No newline at end of file
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index ea7162d4d63..9a8252ab087 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,7 +6,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title "Switch branch/tag"
= dropdown_filter "Search branches and tags"
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 4eaf7c2a025..5254d265918 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -10,69 +10,28 @@
.col-sm-10
= form.check_box :active
-- if @service.supported_events.length > 1
- .form-group
- = form.label :url, "Trigger", class: 'control-label'
- .col-sm-10
- - if @service.supported_events.include?("push")
- %div
- = form.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = form.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This url will be triggered by a push to the repository
- - if @service.supported_events.include?("tag_push")
- %div
- = form.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = form.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- - if @service.supported_events.include?("note")
- %div
- = form.check_box :note_events, class: 'pull-left'
- .prepend-left-20
- = form.label :note_events, class: 'list-label' do
- %strong Comments
- %p.light
- This url will be triggered when someone adds a comment
- - if @service.supported_events.include?("issue")
- %div
- = form.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = form.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This url will be triggered when an issue is created/updated/merged
- - if @service.supported_events.include?("merge_request")
- %div
- = form.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = form.label :merge_requests_events, class: 'list-label' do
- %strong Merge Request events
- %p.light
- This url will be triggered when a merge request is created/updated/merged
- - if @service.supported_events.include?("build")
- %div
- = form.check_box :build_events, class: 'pull-left'
- .prepend-left-20
- = form.label :build_events, class: 'list-label' do
- %strong Build events
- %p.light
- This url will be triggered when a build status changes
- - if @service.supported_events.include?("wiki_page")
- %div
- = form.check_box :wiki_page_events, class: 'pull-left'
- .prepend-left-20
- = form.label :wiki_page_events, class: 'list-label' do
- %strong Wiki Page events
- %p.light
- This url will be triggered when a wiki page is created/updated
+.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.fields.each do |field|
+- @service.global_fields.each do |field|
- type = field[:type]
- if type == 'fieldset'
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index 1c6ec198d3d..107ad19177c 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,7 +1,7 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'control-label' do
Visibility Level
- = link_to "(?)", help_page_path("public_access", "public_access")
+ = link_to "(?)", help_page_path("public_access/public_access")
.col-sm-10
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
diff --git a/app/views/shared/icons/_group.svg b/app/views/shared/icons/_group.svg
deleted file mode 100644
index 75cae0d16c8..00000000000
--- a/app/views/shared/icons/_group.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" 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="#303030">
- <path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path>
- <path d="M5.6667,10.0105 L0.3337,10.0105 C0.1497,10.0105 -0.0003,10.1775 -0.0003,10.3845 L-0.0003,15.6145 C-0.0003,15.8215 0.1497,15.9885 0.3337,15.9885 L5.6667,15.9885 C5.8507,15.9885 5.9997,15.8215 5.9997,15.6145 L5.9997,10.3845 C5.9997,10.1775 5.8507,10.0105 5.6667,10.0105 L5.6667,10.0105 L5.6667,10.0105 Z M1.9997,14.0105 L3.9997,14.0105 L3.9997,12.0105 L1.9997,12.0105 L1.9997,14.0105 L1.9997,14.0105 Z" id="Fill-8"></path>
- <polygon id="Stroke-1" points="12.5 7.5834 3.5 7.5834 3.5 9.5834 12.5 9.5834"></polygon>
- <polygon id="Stroke-3" points="9 9.0834 9 5.0834 7 5.0834 7 9.0834"></polygon>
- <polygon id="Stroke-4" points="4 11.0834 4 7.5834 2 7.5834 2 11.0834"></polygon>
- <polygon id="Stroke-6" points="14 11.0834 14 7.5834 12 7.5834 12 11.0834"></polygon>
- <path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_group.svg.erb b/app/views/shared/icons/_group.svg.erb
new file mode 100644
index 00000000000..53635016900
--- /dev/null
+++ b/app/views/shared/icons/_group.svg.erb
@@ -0,0 +1,13 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16">
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="Group" fill="#303030">
+ <path d="M15.6667,10.0105 L10.3337,10.0105 C10.1497,10.0105 9.9997,10.1775 9.9997,10.3845 L9.9997,15.6145 C9.9997,15.8215 10.1497,15.9885 10.3337,15.9885 L15.6667,15.9885 C15.8507,15.9885 15.9997,15.8215 15.9997,15.6145 L15.9997,10.3845 C15.9997,10.1775 15.8507,10.0105 15.6667,10.0105 L15.6667,10.0105 L15.6667,10.0105 Z M11.9997,14.0105 L13.9997,14.0105 L13.9997,12.0105 L11.9997,12.0105 L11.9997,14.0105 L11.9997,14.0105 Z" id="Fill-11"></path>
+ <path d="M5.6667,10.0105 L0.3337,10.0105 C0.1497,10.0105 -0.0003,10.1775 -0.0003,10.3845 L-0.0003,15.6145 C-0.0003,15.8215 0.1497,15.9885 0.3337,15.9885 L5.6667,15.9885 C5.8507,15.9885 5.9997,15.8215 5.9997,15.6145 L5.9997,10.3845 C5.9997,10.1775 5.8507,10.0105 5.6667,10.0105 L5.6667,10.0105 L5.6667,10.0105 Z M1.9997,14.0105 L3.9997,14.0105 L3.9997,12.0105 L1.9997,12.0105 L1.9997,14.0105 L1.9997,14.0105 Z" id="Fill-8"></path>
+ <polygon id="Stroke-1" points="12.5 7.5834 3.5 7.5834 3.5 9.5834 12.5 9.5834"></polygon>
+ <polygon id="Stroke-3" points="9 9.0834 9 5.0834 7 5.0834 7 9.0834"></polygon>
+ <polygon id="Stroke-4" points="4 11.0834 4 7.5834 2 7.5834 2 11.0834"></polygon>
+ <polygon id="Stroke-6" points="14 11.0834 14 7.5834 12 7.5834 12 11.0834"></polygon>
+ <path d="M11.6667,6.21724894e-15 L4.3337,6.21724894e-15 C4.1497,6.21724894e-15 3.9997,0.167 3.9997,0.374 L3.9997,6.604 C3.9997,6.811 4.1497,6.978 4.3337,6.978 L11.6667,6.978 C11.8507,6.978 11.9997,6.811 11.9997,6.604 L11.9997,0.374 C11.9997,0.167 11.8507,6.21724894e-15 11.6667,6.21724894e-15 L11.6667,6.21724894e-15 L11.6667,6.21724894e-15 Z M5.9997,5 L9.9997,5 L9.9997,2 L5.9997,2 L5.9997,5 L5.9997,5 Z" id="Fill-14"></path>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_icon_commit.svg b/app/views/shared/icons/_icon_commit.svg
new file mode 100644
index 00000000000..0e96035b7b7
--- /dev/null
+++ b/app/views/shared/icons/_icon_commit.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40">
+ <path fill="#8F8F8F" fill-rule="evenodd" d="M28.7769836,18 C27.8675252,13.9920226 24.2831748,11 20,11 C15.7168252,11 12.1324748,13.9920226 11.2230164,18 L4.0085302,18 C2.90195036,18 2,18.8954305 2,20 C2,21.1122704 2.8992496,22 4.0085302,22 L11.2230164,22 C12.1324748,26.0079774 15.7168252,29 20,29 C24.2831748,29 27.8675252,26.0079774 28.7769836,22 L35.9914698,22 C37.0980496,22 38,21.1045695 38,20 C38,18.8877296 37.1007504,18 35.9914698,18 L28.7769836,18 L28.7769836,18 Z M20,25 C22.7614237,25 25,22.7614237 25,20 C25,17.2385763 22.7614237,15 20,15 C17.2385763,15 15,17.2385763 15,20 C15,22.7614237 17.2385763,25 20,25 L20,25 Z"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_fork.svg b/app/views/shared/icons/_icon_fork.svg
new file mode 100644
index 00000000000..fc970e4ce50
--- /dev/null
+++ b/app/views/shared/icons/_icon_fork.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="30" height="40" viewBox="5 0 30 40">
+ <path fill="#7E7E7E" fill-rule="evenodd" d="M22,29.5351288 L22,22.7193602 C26.1888699,21.5098039 29.3985457,16.802989 29.3985457,16.802989 C29.740988,16.3567547 30,15.5559546 30,15.0081969 L30,10.4648712 C31.1956027,9.77325238 32,8.48056471 32,7 C32,4.790861 30.209139,3 28,3 C25.790861,3 24,4.790861 24,7 C24,8.48056471 24.8043973,9.77325238 26,10.4648712 L26,14.7083871 C26,14.8784435 25.9055559,15.0987329 25.7890533,15.2104147 C25.7890533,15.2104147 24.5373893,16.4126202 23.9488702,16.9515733 C22.5015398,18.2770075 21.1191354,19 20.090554,19 C19.0477772,19 17.6172728,18.2608988 16.1128852,16.9142923 C15.5030182,16.3683886 14.3672121,15.3403307 14.3672121,15.3403307 C14.1659605,15.1583364 14.0000086,14.7846305 14.0000192,14.5088473 C14.0000192,14.5088473 14.0000932,12.7539451 14.0001308,10.4647956 C15.1956614,9.77315812 16,8.48051074 16,7 C16,4.790861 14.209139,3 12,3 C9.790861,3 8,4.790861 8,7 C8,8.48056471 8.80439726,9.77325238 10,10.4648712 L10,15.0081969 C10,15.5446944 10.2736352,16.3534183 10.6111812,16.7893819 C10.6111812,16.7893819 13.8599776,21.3779363 18,22.6668724 L18,29.5351288 C16.8043973,30.2267476 16,31.5194353 16,33 C16,35.209139 17.790861,37 20,37 C22.209139,37 24,35.209139 24,33 C24,31.5194353 23.1956027,30.2267476 22,29.5351288 Z M14,7 C14,5.8954305 13.1045695,5 12,5 C10.8954305,5 10,5.8954305 10,7 C10,8.1045695 10.8954305,9 12,9 C13.1045695,9 14,8.1045695 14,7 Z M30,7 C30,5.8954305 29.1045695,5 28,5 C26.8954305,5 26,5.8954305 26,7 C26,8.1045695 26.8954305,9 28,9 C29.1045695,9 30,8.1045695 30,7 Z M22,33 C22,31.8954305 21.1045695,31 20,31 C18.8954305,31 18,31.8954305 18,33 C18,34.1045695 18.8954305,35 20,35 C21.1045695,35 22,34.1045695 22,33 Z"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
new file mode 100644
index 00000000000..80a6d41dbf6
--- /dev/null
+++ b/app/views/shared/icons/_icon_play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_cancel.svg b/app/views/shared/icons/_icon_status_cancel.svg
new file mode 100644
index 00000000000..fd1ebbcbabd
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_cancel.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" 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>
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
new file mode 100644
index 00000000000..e56e0887416
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_failed.svg
@@ -0,0 +1,6 @@
+<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>
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
new file mode 100644
index 00000000000..117f0367161
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_pending.svg
@@ -0,0 +1,6 @@
+<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>
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
new file mode 100644
index 00000000000..920d7952eb5
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_running.svg
@@ -0,0 +1,6 @@
+<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>
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
new file mode 100644
index 00000000000..67b378b3571
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_success.svg
@@ -0,0 +1,6 @@
+<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>
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
new file mode 100644
index 00000000000..d0ad4bd65b1
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_warning.svg
@@ -0,0 +1,6 @@
+<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>
diff --git a/app/views/shared/icons/_icon_timer.svg b/app/views/shared/icons/_icon_timer.svg
new file mode 100644
index 00000000000..0b1e5804427
--- /dev/null
+++ b/app/views/shared/icons/_icon_timer.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_next_discussion.svg b/app/views/shared/icons/_next_discussion.svg
new file mode 100644
index 00000000000..43559a60cb0
--- /dev/null
+++ b/app/views/shared/icons/_next_discussion.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg>
diff --git a/app/views/shared/icons/_project.svg b/app/views/shared/icons/_project.svg
deleted file mode 100644
index 1e8b43f8c6b..00000000000
--- a/app/views/shared/icons/_project.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>Page 1</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_project.svg.erb b/app/views/shared/icons/_project.svg.erb
new file mode 100644
index 00000000000..2f60bb7245e
--- /dev/null
+++ b/app/views/shared/icons/_project.svg.erb
@@ -0,0 +1,3 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 16 16">
+ <path d="M6,6 L12,6 L12,5 L6,5 L6,6 Z M6,8 L12,8 L12,7 L6,7 L6,8 Z M6,10 L12,10 L12,9 L6,9 L6,10 Z M6,12 L12,12 L12,11 L6,11 L6,12 Z M4,6 L5,6 L5,5 L4,5 L4,6 Z M4,8 L5,8 L5,7 L4,7 L4,8 Z M4,10 L5,10 L5,9 L4,9 L4,10 Z M4,12 L5,12 L5,11 L4,11 L4,12 Z M13,3 L10,3 L10,4 L6,4 L6,3 L3,3 L3,13 L13,13 L13,3 Z M2,14 L14,14 L14,2 L2,2 L2,14 Z M1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 L1,0 Z" fill="#7F7E7E" fill-rule="evenodd"></path>
+</svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 094d6636c66..4f8ea7e7cef 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -27,7 +27,17 @@
= render "shared/issuable/label_dropdown"
.pull-right
- = render 'shared/sort_dropdown'
+ - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
+ .dropdown
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ Create new 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" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
+ - else
+ = render 'shared/sort_dropdown'
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
@@ -44,9 +54,15 @@
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
-
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, show_footer: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
+ .filter-item.inline
+ = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
+ %ul
+ %li
+ %a{href: "#", data: {id: "subscribe"}} Subscribe
+ %li
+ %a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
@@ -63,6 +79,7 @@
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
+ new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index c30bdb0ae91..22594b46443 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,8 +1,30 @@
= form_errors(issuable)
+- if @conflict
+ .alert.alert-danger
+ Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
+ Please check out
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs
+
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10
+
+ - issuable_template_names = issuable_templates(issuable)
+
+ - if issuable_template_names.any?
+ .col-sm-3.col-lg-2
+ .js-issuable-selector-wrap{ data: { issuable_type: issuable.class.to_s.underscore.downcase } }
+ - title = selected_template(issuable) || "Choose a template"
+
+ = dropdown_tag(title, options: { toggle_class: 'js-issuable-selector',
+ title: title, filter: true, placeholder: 'Filter', footer_content: true,
+ data: { data: issuable_template_names, field_name: 'issuable_template', selected: selected_template(issuable), project_path: @project.path, namespace_path: @project.namespace.path } } ) do
+ %ul.dropdown-footer-list
+ %li
+ %a.reset-template
+ Reset template
+ %div{ class: issuable_template_names.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
@@ -23,6 +45,13 @@
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
@@ -30,8 +59,9 @@
= 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..."
- = render 'projects/notes/hints'
+ 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
@@ -98,7 +128,7 @@
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
+ = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
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.' }
@@ -149,3 +179,5 @@
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} 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
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index 0acb8253139..c0dc63be2bf 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -2,9 +2,17 @@
- 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')
+- show_boards_content = local_assigns.fetch(:show_boards_content, false)
.dropdown-page-one
= dropdown_title(title)
- = dropdown_filter(filter_placeholder, search_id: "label-name")
+ - 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.
+ = dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
= dropdown_footer do
@@ -12,7 +20,7 @@
- if can?(current_user, :admin_label, @project)
%li
%a.dropdown-toggle-page{href: "#"}
- Create new
+ Create new label
%li
= link_to namespace_project_labels_path(@project.namespace, @project), :"data-is-link" => true do
- if show_create && @project && can?(current_user, :admin_label, @project)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index adfab1af53e..c1b50e65af5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -19,7 +19,7 @@
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.to_reference if issuable.assignee)}
+ .sidebar-collapsed-icon.sidebar-collapsed-user{data: {toggle: "tooltip", placement: "left", container: "body"}, title: (issuable.assignee.name if issuable.assignee)}
- if issuable.assignee
= link_to_member(@project, issuable.assignee, size: 24)
- else
@@ -109,7 +109,7 @@
- if issuable.project.labels.any?
.block.labels
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
= issuable.labels_array.size
@@ -156,7 +156,7 @@
- project_ref = cross_project_reference(@project, issuable)
.block.project-reference
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.dont-change-state
= clipboard_button(clipboard_text: project_ref)
.cross-project-reference.hide-collapsed
%span
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 5ae485f36ba..5f20e4bd42a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,4 +1,4 @@
-- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
+- show_roles = local_assigns.fetch(:show_roles, true)
- show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user
@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
- title: 'Edit access level'
+ title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
+ - if member.expires?
+ ·
+ %span{ class: ('text-warning' if member.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -73,8 +77,16 @@
- if show_roles
.edit-member.hide.js-toggle-content
%br
- = form_for member, remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+ .col-sm-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+ .form-group
+ = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ %i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 975c74f4ea6..dee2472fa79 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -26,7 +26,6 @@
%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
- %i.fa.fa-plus
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
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index b8b66d08db8..66c309644a7 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -12,19 +12,21 @@
%li.project-row{ class: css_class }
= cache(cache_key) do
.controls
+ - if project.archived
+ %span.label.label-warning archived
- if project.commit.try(:status)
%span
= render_commit_status(project.commit)
- if forks
%span
= icon('code-fork')
- = project.forks_count
+ = number_with_delimiter(project.forks_count)
- if stars
%span
= icon('star')
- = project.star_count
+ = number_with_delimiter(project.star_count)
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
- = visibility_level_icon(project.visibility_level, fw: false)
+ = visibility_level_icon(project.visibility_level, fw: true)
.title
= link_to project_path(project), class: dom_class(project) do
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 47ec09f62c6..0c788032020 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,3 +1,7 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('snippet/snippet_bundle.js')
+
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
= form_errors(@snippet)
@@ -31,8 +35,3 @@
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-:javascript
- var editor = ace.edit("editor");
- $(".snippet-form-holder form").submit(function(){
- $(".snippet-file-content").val(editor.getValue());
- });
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d1e861ca80c..d2ec6c3ddef 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -6,7 +6,7 @@
%h4.prepend-top-0
= page_title
%p
- #{link_to "Webhooks", help_page_path("web_hooks", "web_hooks")} can be
+ #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
@@ -19,7 +19,7 @@
= f.label :token, "Secret Token", class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: ''
%p.help-block
- Use this token to validate received payloads
+ Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group
= f.label :url, "Trigger", class: 'label-light'
%ul.list-unstyled
@@ -29,49 +29,56 @@
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
- This url will be triggered by a push to the repository
+ This URL will be triggered by a push to the repository
%li
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
- This url will be triggered when a new tag is pushed to the repository
+ This URL will be triggered when a new tag is pushed to the repository
%li
= f.check_box :note_events, class: 'pull-left'
.prepend-left-20
= f.label :note_events, class: 'list-label' do
%strong Comments
%p.light
- This url will be triggered when someone adds a comment
+ This URL will be triggered when someone adds a comment
%li
= f.check_box :issues_events, class: 'pull-left'
.prepend-left-20
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created/updated/merged
+ This URL will be triggered when an issue is created/updated/merged
%li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created/updated/merged
+ This URL will be triggered when a merge request is created/updated/merged
%li
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
= f.label :build_events, class: 'list-label' do
%strong Build events
%p.light
- This url will be triggered when the build status changes
+ This URL will be triggered when the build status changes
+ %li
+ = f.check_box :pipeline_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :pipeline_events, class: 'list-label' do
+ %strong Pipeline events
+ %p.light
+ This URL will be triggered when the pipeline status changes
%li
= f.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events
%p.light
- This url will be triggered when a wiki page is created/updated
+ This URL will be triggered when a wiki page is created/updated
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
.checkbox
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index cbb8dfb7829..8f7b42eb351 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -28,10 +28,15 @@
%script#js-register-u2f-registered{ type: "text/template" }
%div.row.append-bottom-10
- %p Your device was successfully set up! Click this button to register with the GitLab server.
- = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
- = hidden_field_tag :device_response, nil, class: 'form-control', required: true, id: "js-device-response"
- = submit_tag "Register U2F Device", class: "btn btn-success"
+ .col-md-12
+ %p Your device was successfully set up! Give it a name and register it with the GitLab server.
+ = form_tag(create_u2f_profile_two_factor_auth_path, method: :post) do
+ .row.append-bottom-10
+ .col-md-3
+ = text_field_tag 'u2f_registration[name]', nil, class: 'form-control', placeholder: "Pick a name"
+ .col-md-3
+ = hidden_field_tag 'u2f_registration[device_response]', nil, class: 'form-control', required: true, id: "js-device-response"
+ = submit_tag "Register U2F Device", class: "btn btn-success"
:javascript
var u2fRegister = new U2FRegister($("#js-register-u2f"), gon.u2f);
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index db2b4885861..c7f39868e71 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -2,7 +2,7 @@
- page_description @user.bio
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/d3.js')
- = page_specific_javascript_tag('users/application.js')
+ = page_specific_javascript_tag('users/users_bundle.js')
- header_title @user.name, user_path(@user)
- @no_container = true
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index f2649e38eb3..842eebdea9e 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -21,31 +21,35 @@ class EmailReceiverWorker
return unless raw.present?
can_retry = false
- reason = nil
-
- case e
- when Gitlab::Email::Receiver::SentNotificationNotFoundError
- reason = "We couldn't figure out what the email is in reply to. Please create your comment through the web interface."
- when Gitlab::Email::Receiver::EmptyEmailError
- can_retry = true
- reason = "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies."
- when Gitlab::Email::Receiver::AutoGeneratedEmailError
- reason = "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface."
- when Gitlab::Email::Receiver::UserNotFoundError
- reason = "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface."
- when Gitlab::Email::Receiver::UserBlockedError
- reason = "Your account has been blocked. If you believe this is in error, contact a staff member."
- when Gitlab::Email::Receiver::UserNotAuthorizedError
- reason = "You are not allowed to respond to the thread you are replying to. If you believe this is in error, contact a staff member."
- when Gitlab::Email::Receiver::NoteableNotFoundError
- reason = "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
- when Gitlab::Email::Receiver::InvalidNoteError
- can_retry = true
- reason = e.message
- else
- return
+ reason =
+ case e
+ when Gitlab::Email::UnknownIncomingEmail
+ "We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
+ when Gitlab::Email::SentNotificationNotFoundError
+ "We couldn't figure out what the email is in reply to. Please create your comment through the web interface."
+ when Gitlab::Email::ProjectNotFound
+ "We couldn't find the project. Please check if there's any typo."
+ when Gitlab::Email::EmptyEmailError
+ can_retry = true
+ "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies."
+ when Gitlab::Email::AutoGeneratedEmailError
+ "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface."
+ when Gitlab::Email::UserNotFoundError
+ "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface."
+ when Gitlab::Email::UserBlockedError
+ "Your account has been blocked. If you believe this is in error, contact a staff member."
+ when Gitlab::Email::UserNotAuthorizedError
+ "You are not allowed to perform this action. If you believe this is in error, contact a staff member."
+ when Gitlab::Email::NoteableNotFoundError
+ "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
+ when Gitlab::Email::InvalidNoteError,
+ Gitlab::Email::InvalidIssueError
+ can_retry = true
+ e.message
+ end
+
+ if reason
+ EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later
end
-
- EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 8551288e2f2..1dc7e0adef7 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -28,30 +28,19 @@ class EmailsOnPushWorker
:push
end
- merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha)
-
diff_refs = nil
compare = nil
reverse_compare = false
- if action == :push
- compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
- diff_refs = Gitlab::Diff::DiffRefs.new(
- base_sha: merge_base_sha,
- start_sha: before_sha,
- head_sha: after_sha
- )
+ if action == :push
+ compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
-
- diff_refs = Gitlab::Diff::DiffRefs.new(
- base_sha: merge_base_sha,
- start_sha: after_sha,
- head_sha: before_sha
- )
+ compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
new file mode 100644
index 00000000000..a6cefd4d601
--- /dev/null
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -0,0 +1,16 @@
+class GitGarbageCollectWorker
+ include Sidekiq::Worker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options queue: :gitlab_shell, retry: false
+
+ def perform(project_id)
+ project = Project.find(project_id)
+
+ gitlab_shell.gc(project.repository_storage_path, project.path_with_namespace)
+ # Refresh the branch cache in case garbage collection caused a ref lookup to fail
+ project.repository.after_create_branch
+ project.repository.branch_names
+ project.repository.has_visible_content?
+ end
+end
diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb
deleted file mode 100644
index 1d91897d520..00000000000
--- a/app/workers/gitlab_remove_project_export_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class GitlabRemoveProjectExportWorker
- include Sidekiq::Worker
-
- sidekiq_options queue: :default
-
- def perform
- Project.remove_gitlab_exports!
- end
-end
diff --git a/app/workers/gitlab_shell_one_shot_worker.rb b/app/workers/gitlab_shell_one_shot_worker.rb
deleted file mode 100644
index 4ddbcf574d5..00000000000
--- a/app/workers/gitlab_shell_one_shot_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class GitlabShellOneShotWorker
- include Sidekiq::Worker
- include Gitlab::ShellAdapter
-
- sidekiq_options queue: :gitlab_shell, retry: false
-
- def perform(action, *arg)
- gitlab_shell.send(action, *arg)
- end
-end
diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb
new file mode 100644
index 00000000000..5048746f09b
--- /dev/null
+++ b/app/workers/group_destroy_worker.rb
@@ -0,0 +1,17 @@
+class GroupDestroyWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(group_id, user_id)
+ begin
+ group = Group.with_deleted.find(group_id)
+ rescue ActiveRecord::RecordNotFound
+ return
+ end
+
+ user = User.find(user_id)
+
+ DestroyGroupService.new(group, user).execute
+ end
+end
diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb
new file mode 100644
index 00000000000..72e3a9ae734
--- /dev/null
+++ b/app/workers/import_export_project_cleanup_worker.rb
@@ -0,0 +1,9 @@
+class ImportExportProjectCleanupWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ ImportExportCleanUpService.new.execute
+ end
+end
diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb
index 605ec4f04e5..19f38358eb5 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -141,8 +141,10 @@ class IrkerWorker
end
def files_count(commit)
- files = "#{commit.diffs.real_size} file"
- files += 's' if commit.diffs.count > 1
+ diffs = commit.raw_diffs(deltas_only: true)
+
+ files = "#{diffs.real_size} file"
+ files += 's' if diffs.size > 1
files
end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 09035a7cf2d..a9a2b716005 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -10,6 +10,10 @@ class PostReceive
log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
end
+ changes = Base64.decode64(changes) unless changes.include?(' ')
+ # Use Sidekiq.logger so arguments can be correlated with execution
+ # time and thread ID's.
+ Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
if post_received.project.nil?
diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb
index b51c6a266c9..3062301a9b1 100644
--- a/app/workers/project_destroy_worker.rb
+++ b/app/workers/project_destroy_worker.rb
@@ -12,6 +12,6 @@ class ProjectDestroyWorker
user = User.find(user_id)
- ::Projects::DestroyService.new(project, user, params).execute
+ ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute
end
end
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 39f6037e077..615311e63f5 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -1,7 +1,7 @@
class ProjectExportWorker
include Sidekiq::Worker
- sidekiq_options queue: :gitlab_shell, retry: true
+ sidekiq_options queue: :gitlab_shell, retry: 3
def perform(current_user_id, project_id)
current_user = User.find(current_user_id)
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 00000000000..246c8b6650a
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+ include Sidekiq::Worker
+
+ def perform
+ ProjectGroupLink.expired.destroy_all
+ end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 00000000000..cf765af97ce
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+ include Sidekiq::Worker
+
+ def perform
+ Member.expired.find_each do |member|
+ begin
+ Members::AuthorizedDestroyService.new(member).execute
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_archive_cache_worker.rb b/app/workers/repository_archive_cache_worker.rb
index 47c5a670ed4..a2e49c61f59 100644
--- a/app/workers/repository_archive_cache_worker.rb
+++ b/app/workers/repository_archive_cache_worker.rb
@@ -4,6 +4,6 @@ class RepositoryArchiveCacheWorker
sidekiq_options queue: :default
def perform
- Repository.clean_old_archives
+ RepositoryArchiveCleanUpService.new.execute
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index f7604e48f83..61ed1c38ac4 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -4,7 +4,11 @@ class RepositoryForkWorker
sidekiq_options queue: :gitlab_shell
- def perform(project_id, source_path, target_path)
+ def perform(project_id, forked_from_repository_storage_path, source_path, target_path)
+ Gitlab::Metrics.add_event(:fork_repository,
+ source_path: source_path,
+ target_path: target_path)
+
project = Project.find_by_id(project_id)
unless project.present?
@@ -12,7 +16,8 @@ class RepositoryForkWorker
return
end
- result = gitlab_shell.fork_repository(project.repository_storage_path, source_path, target_path)
+ result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_path,
+ project.repository_storage_path, target_path)
unless result
logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
project.mark_import_as_failed('The project could not be forked.')
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 7d819fe78f8..d2ca8813ab9 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -10,6 +10,12 @@ class RepositoryImportWorker
@project = Project.find(project_id)
@current_user = @project.creator
+ Gitlab::Metrics.add_event(:import_repository,
+ import_url: @project.import_url,
+ path: @project.path_with_namespace)
+
+ project.update_column(:import_error, nil)
+
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
diff --git a/app/workers/requests_profiles_worker.rb b/app/workers/requests_profiles_worker.rb
new file mode 100644
index 00000000000..9dd228a2483
--- /dev/null
+++ b/app/workers/requests_profiles_worker.rb
@@ -0,0 +1,9 @@
+class RequestsProfilesWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 21e7cc7b6e8..4792f6670a8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -76,17 +76,23 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
- config.assets.paths << Gemojione.index.images_path
+ config.assets.paths << Gemojione.images_path
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
- config.assets.precompile << "graphs/application.js"
- config.assets.precompile << "users/application.js"
- config.assets.precompile << "network/application.js"
- config.assets.precompile << "profile/application.js"
+ config.assets.precompile << "graphs/graphs_bundle.js"
+ config.assets.precompile << "users/users_bundle.js"
+ config.assets.precompile << "network/network_bundle.js"
+ config.assets.precompile << "profile/profile_bundle.js"
+ config.assets.precompile << "diff_notes/diff_notes_bundle.js"
+ config.assets.precompile << "boards/boards_bundle.js"
+ config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "blob_edit/blob_edit_bundle.js"
+ config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
+ config.assets.precompile << "u2f.js"
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
@@ -106,7 +112,8 @@ module Gitlab
end
end
- redis_config_hash = Gitlab::Redis.redis_store_options
+ # Use Redis caching across all environments
+ redis_config_hash = Gitlab::Redis.params
redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 293f2b71d65..74325872b09 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -68,6 +68,25 @@
:why: https://opensource.org/licenses/BSD-2-Clause
:versions: []
:when: 2016-05-02 05:55:09.796363000 Z
+- - :whitelist
+ - LGPLv2+
+ - :who: Stan Hu
+ :why: Equivalent to LGPLv2
+ :versions: []
+ :when: 2016-06-07 17:14:10.907682000 Z
+- - :whitelist
+ - Artistic 2.0
+ - :who: Josh Frye
+ :why: Disk/mount information display on Admin pages
+ :versions: []
+ :when: 2016-06-29 16:32:45.432113000 Z
+- - :whitelist
+ - Simplified BSD
+ - :who: Douwe Maan
+ :why: https://opensource.org/licenses/BSD-2-Clause
+ :versions: []
+ :when: 2016-07-26 21:24:07.248480000 Z
+
# LICENSE BLACKLIST
- - :blacklist
@@ -175,15 +194,3 @@
:why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc
:versions: []
:when: 2016-05-02 05:56:50.696858000 Z
-- - :whitelist
- - LGPLv2+
- - :who: Stan Hu
- :why: Equivalent to LGPLv2
- :versions: []
- :when: 2016-06-07 17:14:10.907682000 Z
-- - :whitelist
- - Artistic 2.0
- - :who: Josh Frye
- :why: Disk/mount information display on Admin pages
- :versions: []
- :when: 2016-06-29 16:32:45.432113000 Z
diff --git a/config/gitlab.teatro.yml b/config/gitlab.teatro.yml
deleted file mode 100644
index 75b79b837e0..00000000000
--- a/config/gitlab.teatro.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-
-production: &base
- gitlab:
- host: localhost
- port: 80
- https: false
-
- user: root
-
- email_from: example@example.com
-
- support_email: support@example.com
-
- default_projects_features:
- issues: true
- merge_requests: true
- wiki: true
- snippets: false
- visibility_level: "private" # can be "private" | "internal" | "public"
-
- issues_tracker:
-
- gravatar:
- enabled: true # Use user avatar image from Gravatar.com (default: true)
-
- ldap:
- enabled: false
- host: '_your_ldap_server'
- port: 636
- uid: 'sAMAccountName'
- method: 'ssl' # "tls" or "ssl" or "plain"
- bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
- password: '_the_password_of_the_bind_user'
- allow_username_or_email_login: true
-
- base: ''
-
- user_filter: ''
-
- omniauth:
- enabled: false
-
- satellites:
- # Relative paths are relative to Rails.root (default: tmp/repo_satellites/)
- path: /apps/gitlab-satellites/
-
- backup:
- path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/)
-
- repositories:
- storages: # REPO PATHS MUST NOT BE A SYMLINK!!!
- default: /apps/repositories/
-
- gitlab_shell:
- path: /apps/gitlab-shell/
-
- hooks_path: /apps/gitlab-shell/hooks/
-
- upload_pack: true
- receive_pack: true
-
- git:
- bin_path: /usr/bin/git
- max_size: 5242880 # 5.megabytes
- timeout: 10
-
- extra:
-
-development:
- <<: *base
-
-test:
- <<: *base
- gravatar:
- enabled: true
- gitlab:
- host: localhost
- port: 80
- issues_tracker:
- redmine:
- title: "Redmine"
- project_url: "http://redmine/projects/:issues_tracker_id"
- issues_url: "http://redmine/:project_id/:issues_tracker_id/:id"
- new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new"
-
-staging:
- <<: *base
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 325eca72862..1470a6e2550 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -106,8 +106,8 @@ production: &base
## Repository downloads directory
# When a user clicks e.g. 'Download zip' on a project, a temporary zip file is created in the following directory.
- # The default is 'tmp/repositories' relative to the root of the Rails app.
- # repository_downloads_path: tmp/repositories
+ # The default is 'shared/cache/archive/' relative to the root of the Rails app.
+ # repository_downloads_path: shared/cache/archive/
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 51d93e8cde0..4a01b9e40fb 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -211,9 +211,8 @@ Settings.gitlab.default_projects_features['snippets'] = false if Setti
Settings.gitlab.default_projects_features['builds'] = true if Settings.gitlab.default_projects_features['builds'].nil?
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
-Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
-Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
+Settings.gitlab['domain_whitelist'] ||= []
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
#
@@ -288,9 +287,18 @@ Settings.cron_jobs['admin_email_worker']['job_class'] = 'AdminEmailWorker'
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
-Settings.cron_jobs['gitlab_remove_project_export_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['gitlab_remove_project_export_worker']['cron'] ||= '0 * * * *'
-Settings.cron_jobs['gitlab_remove_project_export_worker']['job_class'] = 'GitlabRemoveProjectExportWorker'
+Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *'
+Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker'
+Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
+Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
+Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
+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'
#
# GitLab Shell
@@ -316,6 +324,21 @@ Settings.repositories['storages'] ||= {}
Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/'
#
+# The repository_downloads_path is used to remove outdated repository
+# archives, if someone has it configured incorrectly, and it points
+# to the path where repositories are stored this can cause some
+# data-integrity issue. In this case, we sets it to the default
+# repository_downloads_path value.
+#
+repositories_storages_path = Settings.repositories.storages.values
+repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '')
+repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
+
+if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) }
+ Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
+end
+
+#
# Backup
#
Settings['backup'] ||= Settingslogic.new({})
diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb
index e026151a032..ed88c8ee1b8 100644
--- a/config/initializers/5_backend.rb
+++ b/config/initializers/5_backend.rb
@@ -1,6 +1,3 @@
-# GIT over HTTP
-require_dependency Rails.root.join('lib/gitlab/backend/grack_auth')
-
# GIT over SSH
require_dependency Rails.root.join('lib/gitlab/backend/shell')
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index 3ba9e36c567..d92f64e1647 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -3,22 +3,27 @@ def storage_name_valid?(name)
end
def find_parent_path(name, path)
+ parent = Pathname.new(path).realpath.parent
Gitlab.config.repositories.storages.detect do |n, p|
- name != n && path.chomp('/').start_with?(p.chomp('/'))
+ name != n && Pathname.new(p).realpath == parent
end
end
-def error(message)
+def storage_validation_error(message)
raise "#{message}. Please fix this in your gitlab.yml before starting GitLab."
end
-error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
+def validate_storages
+ storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
-Gitlab.config.repositories.storages.each do |name, path|
- error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
+ Gitlab.config.repositories.storages.each do |name, path|
+ storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- parent_name, _parent_path = find_parent_path(name, path)
- if parent_name
- error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
+ parent_name, _parent_path = find_parent_path(name, path)
+ if parent_name
+ storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
+ end
end
end
+
+validate_storages unless Rails.env.test? || ENV['SKIP_STORAGE_VALIDATION'] == 'true'
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
new file mode 100644
index 00000000000..0da584626ee
--- /dev/null
+++ b/config/initializers/ar_monkey_patch.rb
@@ -0,0 +1,57 @@
+# rubocop:disable Lint/RescueException
+
+# This patch fixes https://github.com/rails/rails/issues/26024
+# TODO: Remove it when it's no longer necessary
+
+module ActiveRecord
+ module Locking
+ module Optimistic
+ # We overwrite this method because we don't want to have default value
+ # for newly created records
+ def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ super
+ end
+
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ return super unless locking_enabled?
+ return 0 if attribute_names.empty?
+
+ lock_col = self.class.locking_column
+
+ previous_lock_value = send(lock_col).to_i
+
+ # This line is added as a patch
+ previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
+
+ increment_lock
+
+ attribute_names += [lock_col]
+ attribute_names.uniq!
+
+ begin
+ relation = self.class.unscoped
+
+ affected_rows = relation.where(
+ self.class.primary_key => id,
+ lock_col => previous_lock_value,
+ ).update_all(
+ attributes_for_update(attribute_names).map do |name|
+ [name, _read_attribute(name)]
+ end.to_h
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError.new(self, "update")
+ end
+
+ affected_rows
+
+ # If something went wrong, revert the version.
+ rescue Exception
+ send(lock_col + '=', previous_lock_value)
+ raise
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 73977341b73..a0a8f88584c 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -100,6 +100,9 @@ Devise.setup do |config|
# secure: true in order to force SSL only cookies.
# config.cookie_options = {}
+ # Send a notification email when the user's password is changed
+ config.send_password_change_notification = true
+
# ==> Configuration for :validatable
# Range for password length. Default is 6..128.
config.password_length = 8..128
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 618dba74151..fc4b0a72add 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -12,7 +12,8 @@ Doorkeeper.configure do
end
resource_owner_from_credentials do |routes|
- Gitlab::Auth.find_with_user_password(params[:username], params[:password])
+ user = Gitlab::Auth.find_with_user_password(params[:username], params[:password])
+ user unless user.try(:two_factor_enabled?)
end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.
diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb
index 6796407d4e6..4c91a61fb4a 100644
--- a/config/initializers/health_check.rb
+++ b/config/initializers/health_check.rb
@@ -1,16 +1,3 @@
-# Email forcibly included in the standard checks, but the email health check
-# doesn't support the full range of SMTP options, which can result in failures
-# for valid SMTP configurations.
-# Overwrite the HealthCheck's detection of whether email is configured
-# in order to avoid the email check during standard checks
-module HealthCheck
- class Utils
- def self.mailer_configured?
- false
- end
- end
-end
-
HealthCheck.setup do |config|
config.standard_checks = ['database', 'migrations', 'cache']
config.full_checks = ['database', 'migrations', 'cache']
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index c4266ab8ba5..52522e099e7 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -136,7 +136,21 @@ if Gitlab::Metrics.enabled?
config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ config.instrument_methods(namespace_mod)
+ config.instrument_methods(namespace_mod::Document)
+ end
+
config.instrument_methods(Rinku)
+ config.instrument_instance_methods(Repository)
+
+ config.instrument_methods(Gitlab::Highlight)
+ config.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ config.instrument_method(Project, :visible_to_user)
end
GC::Profiler.enable
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index ca58ae92d1b..f498732feca 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -6,5 +6,16 @@
Mime::Type.register_alias "text/plain", :diff
Mime::Type.register_alias "text/plain", :patch
-Mime::Type.register_alias 'text/html', :markdown
-Mime::Type.register_alias 'text/html', :md
+Mime::Type.register_alias "text/html", :markdown
+Mime::Type.register_alias "text/html", :md
+
+Mime::Type.register "video/mp4", :mp4, [], [:m4v, :mov]
+Mime::Type.register "video/webm", :webm
+Mime::Type.register "video/ogg", :ogv
+
+middlewares = Gitlab::Application.config.middleware
+middlewares.swap(ActionDispatch::ParamsParser, ActionDispatch::ParamsParser, {
+ Mime::Type.lookup('application/vnd.git-lfs+json') => lambda do |body|
+ ActiveSupport::JSON.decode(body)
+ end
+})
diff --git a/config/initializers/relative_naming_ci_namespace.rb b/config/initializers/relative_naming_ci_namespace.rb
new file mode 100644
index 00000000000..59abe1b9b91
--- /dev/null
+++ b/config/initializers/relative_naming_ci_namespace.rb
@@ -0,0 +1,16 @@
+# Description: https://coderwall.com/p/heed_q/rails-routing-and-namespaced-models
+#
+# This allows us to use CI ActiveRecord objects in all routes and use it:
+# - [project.namespace, project, build]
+#
+# instead of:
+# - namespace_project_build_path(project.namespace, project, build)
+#
+# Without that, Ci:: namespace is used for resolving routes:
+# - namespace_project_ci_build_path(project.namespace, project, build)
+
+module Ci
+ def self.use_relative_model_naming?
+ true
+ end
+end
diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb
new file mode 100644
index 00000000000..a9aa802681a
--- /dev/null
+++ b/config/initializers/request_profiler.rb
@@ -0,0 +1,5 @@
+require 'gitlab/request_profiler/middleware'
+
+Rails.application.configure do |config|
+ config.middleware.use(Gitlab::RequestProfiler::Middleware)
+end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index dae3a4a9a93..291fa6c0abc 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -2,49 +2,86 @@
require 'securerandom'
-# Your secret key for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-
-def find_secure_token
- token_file = Rails.root.join('.secret')
- if ENV.key?('SECRET_KEY_BASE')
- ENV['SECRET_KEY_BASE']
- elsif File.exist? token_file
- # Use the existing token.
- File.read(token_file).chomp
- else
- # Generate a new token of 64 random hexadecimal characters and store it in token_file.
- token = SecureRandom.hex(64)
- File.write(token_file, token)
- token
+# Transition material in .secret to the secret_key_base key in config/secrets.yml.
+# Historically, ENV['SECRET_KEY_BASE'] takes precedence over .secret, so we maintain that
+# behavior.
+#
+# It also used to be the case that the key material in ENV['SECRET_KEY_BASE'] or .secret
+# was used to encrypt OTP (two-factor authentication) data so if present, we copy that key
+# material into config/secrets.yml under otp_key_base.
+#
+# Finally, if we have successfully migrated all secrets to config/secrets.yml, delete the
+# .secret file to avoid confusion.
+#
+def create_tokens
+ secret_file = Rails.root.join('.secret')
+ file_secret_key = File.read(secret_file).chomp if File.exist?(secret_file)
+ env_secret_key = ENV['SECRET_KEY_BASE']
+
+ # Ensure environment variable always overrides secrets.yml.
+ Rails.application.secrets.secret_key_base = env_secret_key if env_secret_key.present?
+
+ defaults = {
+ secret_key_base: file_secret_key || generate_new_secure_token,
+ otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
+ db_key_base: generate_new_secure_token
+ }
+
+ missing_secrets = set_missing_keys(defaults)
+ write_secrets_yml(missing_secrets) unless missing_secrets.empty?
+
+ begin
+ File.delete(secret_file) if file_secret_key
+ rescue => e
+ warn "Error deleting useless .secret file: #{e}"
end
end
-Rails.application.config.secret_token = find_secure_token
-Rails.application.config.secret_key_base = find_secure_token
-
-# CI
def generate_new_secure_token
SecureRandom.hex(64)
end
-if Rails.application.secrets.db_key_base.blank?
- warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`"
+def warn_missing_secret(secret)
+ warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
+end
- all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml')
- all_secrets ||= {}
+def set_missing_keys(defaults)
+ defaults.stringify_keys.each_with_object({}) do |(key, default), missing|
+ if Rails.application.secrets[key].blank?
+ warn_missing_secret(key)
- # generate secrets
- env_secrets = all_secrets[Rails.env.to_s] || {}
- env_secrets['db_key_base'] ||= generate_new_secure_token
- all_secrets[Rails.env.to_s] = env_secrets
+ missing[key] = Rails.application.secrets[key] = default
+ end
+ end
+end
+
+def write_secrets_yml(missing_secrets)
+ secrets_yml = Rails.root.join('config/secrets.yml')
+ rails_env = Rails.env.to_s
+ secrets = YAML.load_file(secrets_yml) if File.exist?(secrets_yml)
+ secrets ||= {}
+ secrets[rails_env] ||= {}
+
+ secrets[rails_env].merge!(missing_secrets) do |key, old, new|
+ # Previously, it was possible this was set to the literal contents of an Erb
+ # expression that evaluated to an empty value. We don't want to support that
+ # specifically, just ensure we don't break things further.
+ #
+ if old.present?
+ warn <<EOM
+Rails.application.secrets.#{key} was blank, but the literal value in config/secrets.yml was:
+ #{old}
- # save secrets
- File.open('config/secrets.yml', 'w', 0600) do |file|
- file.write(YAML.dump(all_secrets))
+This probably isn't the expected value for this secret. To keep using a literal Erb string in config/secrets.yml, replace `<%` with `<%%`.
+EOM
+
+ exit 1 # rubocop:disable Rails/Exit
+ end
+
+ new
end
- Rails.application.secrets.db_key_base = env_secrets['db_key_base']
+ File.write(secrets_yml, YAML.dump(secrets), mode: 'w', perm: 0600)
end
+
+create_tokens
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 0d9d87bac00..70be2617cab 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -13,9 +13,9 @@ end
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
- redis_config = Gitlab::Redis.redis_store_options
+ redis_config = Gitlab::Redis.params
redis_config[:namespace] = Gitlab::Redis::SESSION_NAMESPACE
-
+
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: redis_config,
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 593c14a289f..f7e714cd6bc 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -1,19 +1,29 @@
+# Custom Redis configuration
+redis_config_hash = Gitlab::Redis.params
+redis_config_hash[:namespace] = Gitlab::Redis::SIDEKIQ_NAMESPACE
+
Sidekiq.configure_server do |config|
- config.redis = {
- url: Gitlab::Redis.url,
- namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
- }
+ config.redis = redis_config_hash
config.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
+ chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
end
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
# UGLY hack: Settingslogic doesn't allow 'class' key
- cron_jobs.each { |k, v| cron_jobs[k]['class'] = cron_jobs[k].delete('job_class') }
+ cron_jobs_required_keys = %w(job_class cron)
+ cron_jobs.each do |k, v|
+ if cron_jobs[k] && cron_jobs_required_keys.all? { |s| cron_jobs[k].key?(s) }
+ cron_jobs[k]['class'] = cron_jobs[k].delete('job_class')
+ else
+ cron_jobs.delete(k)
+ Rails.logger.error("Invalid cron_jobs config key: '#{k}'. Check your gitlab config file.")
+ end
+ end
Sidekiq::Cron::Job.load_from_hash! cron_jobs
# Database pool should be at least `sidekiq_concurrency` + 2
@@ -30,8 +40,5 @@ Sidekiq.configure_server do |config|
end
Sidekiq.configure_client do |config|
- config.redis = {
- url: Gitlab::Redis.url,
- namespace: Gitlab::Redis::SIDEKIQ_NAMESPACE
- }
+ config.redis = redis_config_hash
end
diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb
index df4a933e22f..cd869657c53 100644
--- a/config/initializers/trusted_proxies.rb
+++ b/config/initializers/trusted_proxies.rb
@@ -7,10 +7,18 @@ module Rack
class Request
def trusted_proxy?(ip)
Rails.application.config.action_dispatch.trusted_proxies.any? { |proxy| proxy === ip }
+ rescue IPAddr::InvalidAddressError
+ false
end
end
end
+gitlab_trusted_proxies = Array(Gitlab.config.gitlab.trusted_proxies).map do |proxy|
+ begin
+ IPAddr.new(proxy)
+ rescue IPAddr::InvalidAddressError
+ end
+end.compact
+
Rails.application.config.action_dispatch.trusted_proxies = (
- [ '127.0.0.1', '::1' ] + Array(Gitlab.config.gitlab.trusted_proxies)
-).map { |proxy| IPAddr.new(proxy) }
+ [ '127.0.0.1', '::1' ] + gitlab_trusted_proxies)
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 7cab24b295e..c639f8260aa 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -1,47 +1,36 @@
+# If you change this file in a Merge Request, please also create
+# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
+#
:mailboxes:
-<%
-require "yaml"
-require "json"
-require_relative "lib/gitlab/redis" unless defined?(Gitlab::Redis)
+ <%
+ require_relative "lib/gitlab/mail_room" unless defined?(Gitlab::MailRoom)
+ config = Gitlab::MailRoom.config
-rails_env = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
-
-config_file = ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] || "config/gitlab.yml"
-if File.exists?(config_file)
- all_config = YAML.load_file(config_file)[rails_env]
-
- config = all_config["incoming_email"] || {}
- config['enabled'] = false if config['enabled'].nil?
- config['port'] = 143 if config['port'].nil?
- config['ssl'] = false if config['ssl'].nil?
- config['start_tls'] = false if config['start_tls'].nil?
- config['mailbox'] = "inbox" if config['mailbox'].nil?
-
- if config['enabled'] && config['address']
- redis_url = Gitlab::Redis.new(rails_env).url
- %>
+ if Gitlab::MailRoom.enabled?
+ %>
-
- :host: <%= config['host'].to_json %>
- :port: <%= config['port'].to_json %>
- :ssl: <%= config['ssl'].to_json %>
- :start_tls: <%= config['start_tls'].to_json %>
- :email: <%= config['user'].to_json %>
- :password: <%= config['password'].to_json %>
+ :host: <%= config[:host].to_json %>
+ :port: <%= config[:port].to_json %>
+ :ssl: <%= config[:ssl].to_json %>
+ :start_tls: <%= config[:start_tls].to_json %>
+ :email: <%= config[:user].to_json %>
+ :password: <%= config[:password].to_json %>
+ :idle_timeout: 60
- :name: <%= config['mailbox'].to_json %>
+ :name: <%= config[:mailbox].to_json %>
:delete_after_delivery: true
:delivery_method: sidekiq
:delivery_options:
- :redis_url: <%= redis_url.to_json %>
- :namespace: resque:gitlab
+ :redis_url: <%= config[:redis_url].to_json %>
+ :namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
:queue: incoming_email
:worker: EmailReceiverWorker
:arbitration_method: redis
:arbitration_options:
- :redis_url: <%= redis_url.to_json %>
- :namespace: mail_room:gitlab
+ :redis_url: <%= config[:redis_url].to_json %>
+ :namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %>
+
<% end %>
-<% end %>
diff --git a/config/resque.yml.example b/config/resque.yml.example
index d98f43f71b2..0c19d8bc1d3 100644
--- a/config/resque.yml.example
+++ b/config/resque.yml.example
@@ -1,6 +1,34 @@
# If you change this file in a Merge Request, please also create
# a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
#
-development: redis://localhost:6379
-test: redis://localhost:6379
-production: unix:/var/run/redis/redis.sock
+development:
+ url: redis://localhost:6379
+ # sentinels:
+ # -
+ # host: localhost
+ # port: 26380 # point to sentinel, not to redis port
+ # -
+ # host: slave2
+ # port: 26381 # point to sentinel, not to redis port
+test:
+ url: redis://localhost:6379
+production:
+ # Redis (single instance)
+ url: unix:/var/run/redis/redis.sock
+ ##
+ # Redis + Sentinel (for HA)
+ #
+ # Please read instructions carefully before using it as you may lose data:
+ # http://redis.io/topics/sentinel
+ #
+ # You must specify a list of a few sentinels that will handle client connection
+ # please read here for more information: https://docs.gitlab.com/ce/administration/high_availability/redis.html
+ ##
+ # url: redis://master:6379
+ # sentinels:
+ # -
+ # host: slave1
+ # port: 26379 # point to sentinel, not to redis port
+ # -
+ # host: slave2
+ # port: 26379 # point to sentinel, not to redis port
diff --git a/config/routes.rb b/config/routes.rb
index 18a4ead2b37..24f9b44a53a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -42,10 +42,9 @@ Rails.application.routes.draw do
resource :lint, only: [:show, :create]
- resources :projects do
+ resources :projects, only: [:index, :show] do
member do
get :status, to: 'projects#badge'
- get :integration
end
end
@@ -85,14 +84,16 @@ Rails.application.routes.draw do
# Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
- # Enable Grack support (for LFS only)
- mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\/(info\/lfs|gitlab-lfs)/.match(request.path_info) }, via: [:get, :post, :put]
-
# Help
- get 'help' => 'help#index'
- get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ }
- get 'help/shortcuts'
- get 'help/ui' => 'help#ui'
+ get 'help' => 'help#index'
+ get 'help/shortcuts' => 'help#shortcuts'
+ get 'help/ui' => 'help#ui'
+ get 'help/*path' => 'help#show', as: :help_page
+
+ #
+ # Koding route
+ #
+ get 'koding' => 'koding#index'
#
# Global snippets
@@ -144,19 +145,13 @@ Rails.application.routes.draw do
get :jobs
end
- resource :gitlab, only: [:create, :new], controller: :gitlab do
+ resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
get :jobs
end
- resource :bitbucket, only: [:create, :new], controller: :bitbucket do
- get :status
- get :callback
- get :jobs
- end
-
- resource :gitorious, only: [:create, :new], controller: :gitorious do
+ resource :bitbucket, only: [:create], controller: :bitbucket do
get :status
get :callback
get :jobs
@@ -243,7 +238,6 @@ Rails.application.routes.draw do
get :projects
get :keys
get :groups
- put :team_update
put :block
put :unblock
put :unlock
@@ -257,7 +251,11 @@ Rails.application.routes.draw do
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
- resources :spam_logs, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy] do
+ member do
+ post :mark_as_ham
+ end
+ end
resources :applications
@@ -281,6 +279,7 @@ Rails.application.routes.draw do
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
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
@@ -300,7 +299,7 @@ Rails.application.routes.draw do
end
end
- resource :appearances, path: 'appearance' do
+ resource :appearances, only: [:show, :create, :update], path: 'appearance' do
member do
get :preview
delete :logo
@@ -309,7 +308,7 @@ Rails.application.routes.draw do
end
resource :application_settings, only: [:show, :update] do
- resources :services
+ resources :services, only: [:index, :edit, :update]
put :reset_runners_token
put :reset_health_check_token
put :clear_repository_check_states
@@ -346,7 +345,7 @@ Rails.application.routes.draw do
end
scope module: :profiles do
- resource :account, only: [:show, :update] do
+ resource :account, only: [:show] do
member do
delete :unlink
end
@@ -358,7 +357,7 @@ Rails.application.routes.draw do
end
end
resource :preferences, only: [:show, :update]
- resources :keys
+ resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
@@ -375,6 +374,8 @@ Rails.application.routes.draw do
patch :skip
end
end
+
+ resources :u2f_registrations, only: [:destroy]
end
end
@@ -472,7 +473,7 @@ Rails.application.routes.draw do
post :unarchive
post :housekeeping
post :toggle_star
- post :markdown_preview
+ post :preview_markdown
post :export
post :remove_export
post :generate_new_export
@@ -483,11 +484,26 @@ Rails.application.routes.draw do
end
scope module: :projects do
- # Git HTTP clients ('git clone' etc.)
scope constraints: { id: /.+\.git/, format: nil } do
+ # Git HTTP clients ('git clone' etc.)
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
+
+ # Git LFS API (metadata)
+ post '/info/lfs/objects/batch', to: 'lfs_api#batch'
+ post '/info/lfs/objects', to: 'lfs_api#deprecated'
+ get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+
+ # GitLab LFS object storage
+ scope constraints: { oid: /[a-f0-9]{64}/ } do
+ get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+
+ scope constraints: { size: /[0-9]+/ } do
+ put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
+ put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+ end
+ end
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
@@ -513,6 +529,11 @@ Rails.application.routes.draw do
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+ #
+ # Templates
+ #
+ get '/templates/:template_type/:key' => 'templates#show', as: :template
+
scope do
get(
'/blob/*id/diff',
@@ -615,22 +636,31 @@ Rails.application.routes.draw do
post :retry_builds
post :revert
post :cherry_pick
+ get :diff_for_path
end
end
- resources :compare, only: [:index, :create]
- resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }
-
- resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
- member do
- get :commits
- get :ci
- get :languages
+ resources :compare, only: [:index, :create] do
+ collection do
+ get :diff_for_path
end
end
- get '/compare/:from...:to' => 'compare#show', :as => 'compare',
- :constraints => { from: /.+/, to: /.+/ }
+ get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+ # Don't use format parameter as file extension (old 3.0.x behavior)
+ # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
+ scope format: false do
+ resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
+
+ resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ member do
+ get :commits
+ get :ci
+ get :languages
+ end
+ end
+ end
resources :snippets, constraints: { id: /\d+/ } do
member do
@@ -652,10 +682,10 @@ Rails.application.routes.draw do
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/markdown_preview', to: 'wikis#markdown_preview', constraints: WIKI_SLUG_ID, as: 'wiki_markdown_preview'
+ post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
end
- resource :repository, only: [:show, :create] do
+ resource :repository, only: [:create] do
member do
get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
end
@@ -698,7 +728,9 @@ Rails.application.routes.draw do
member do
get :commits
get :diffs
+ get :conflicts
get :builds
+ get :pipelines
get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
@@ -706,12 +738,22 @@ Rails.application.routes.draw do
post :toggle_subscription
post :toggle_award_emoji
post :remove_wip
+ get :diff_for_path
+ post :resolve_conflicts
end
collection do
get :branch_from
get :branch_to
get :update_branches
+ get :diff_for_path
+ end
+
+ resources :discussions, only: [], constraints: { id: /\h{40}/ } do
+ member do
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
end
end
@@ -725,13 +767,17 @@ Rails.application.routes.draw do
resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
+ collection do
+ resource :pipelines_settings, path: 'settings', only: [:show, :update]
+ end
+
member do
post :cancel
post :retry
end
end
- resources :environments, only: [:index, :show, :new, :create, :destroy]
+ resources :environments
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
@@ -742,6 +788,7 @@ Rails.application.routes.draw do
get :status
post :cancel
post :retry
+ post :play
post :erase
get :trace
get :raw
@@ -770,7 +817,7 @@ Rails.application.routes.draw do
end
end
- resources :labels, constraints: { id: /\d+/ } do
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
collection do
post :generate
post :set_priorities
@@ -786,6 +833,7 @@ Rails.application.routes.draw do
member do
post :toggle_subscription
post :toggle_award_emoji
+ post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
@@ -795,7 +843,7 @@ Rails.application.routes.draw do
end
end
- resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
+ resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
@@ -816,6 +864,22 @@ Rails.application.routes.draw do
member do
post :toggle_award_emoji
delete :delete_attachment
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
+ end
+
+ resource :board, only: [:show] do
+ scope module: :boards do
+ resources :issues, only: [:update]
+
+ resources :lists, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index]
+ end
end
end
@@ -842,7 +906,10 @@ Rails.application.routes.draw do
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
- get :build, constraints: { format: /svg/ }
+ constraints format: /svg/ do
+ get :build
+ get :coverage
+ end
end
end
end
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index b4639999967..e3316ecdb6c 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -20,7 +20,6 @@ Sidekiq::Testing.inline! do
'https://github.com/airbnb/javascript.git',
'https://github.com/tessalt/echo-chamber-js.git',
'https://github.com/atom/atom.git',
- 'https://github.com/ipselon/react-ui-builder.git',
'https://github.com/mattermost/platform.git',
'https://github.com/purifycss/purifycss.git',
'https://github.com/facebook/nuclide.git',
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
deleted file mode 100644
index 51ff451eb4c..00000000000
--- a/db/fixtures/development/14_builds.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-class Gitlab::Seeder::Builds
- def initialize(project)
- @project = project
- end
-
- def seed!
- ci_commits.each do |ci_commit|
- begin
- build_create!(ci_commit, name: 'test build 1')
- build_create!(ci_commit, status: 'success', name: 'test build 2')
- print '.'
- rescue ActiveRecord::RecordInvalid
- print 'F'
- end
- end
- end
-
- def ci_commits
- commits = @project.repository.commits('master', nil, 5)
- commits_sha = commits.map { |commit| commit.raw.id }
- commits_sha.map do |sha|
- @project.ensure_pipeline(sha, 'master')
- end
- rescue
- []
- end
-
- def build_create!(ci_commit, opts = {})
- attributes = build_attributes_for(ci_commit).merge(opts)
- build = Ci::Build.new(attributes)
-
- if %w(success failed).include?(build.status)
- artifacts_cache_file(artifacts_archive_path) do |file|
- build.artifacts_file = file
- end
-
- artifacts_cache_file(artifacts_metadata_path) do |file|
- build.artifacts_metadata = file
- end
- end
-
- build.save!
-
- if %w(running success failed).include?(build.status)
- # We need to set build trace after saving a build (id required)
- build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
- end
- end
-
- def build_attributes_for(ci_commit)
- { name: 'test build', commands: "$ build command",
- stage: 'test', stage_idx: 1, ref: 'master',
- user_id: build_user, gl_project_id: @project.id,
- status: build_status, commit_id: ci_commit.id,
- created_at: Time.now, updated_at: Time.now }
- end
-
- def build_user
- @project.team.users.sample
- end
-
- def build_status
- Ci::Build::AVAILABLE_STATUSES.sample
- end
-
- def artifacts_archive_path
- Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
- end
-
- def artifacts_metadata_path
- Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
-
- end
-
- def artifacts_cache_file(file_path)
- cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
-
- FileUtils.copy(file_path, cache_path)
- File.open(cache_path) do |file|
- yield file
- end
- end
-end
-
-Gitlab::Seeder.quiet do
- Project.all.sample(10).each do |project|
- project_builds = Gitlab::Seeder::Builds.new(project)
- project_builds.seed!
- end
-end
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
new file mode 100644
index 00000000000..49e6e2361b1
--- /dev/null
+++ b/db/fixtures/development/14_pipelines.rb
@@ -0,0 +1,153 @@
+class Gitlab::Seeder::Pipelines
+ STAGES = %w[build test deploy notify]
+ BUILDS = [
+ { name: 'build:linux', stage: 'build', status: :success },
+ { name: 'build:osx', stage: 'build', status: :success },
+ { name: 'rspec:linux', stage: 'test', status: :success },
+ { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:osx', stage: 'test', status_event: :success },
+ { name: 'spinach:linux', stage: 'test', status: :success },
+ { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
+ { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
+ { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
+ { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
+ { name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
+ { name: 'slack', stage: 'notify', when: 'manual', status: :created },
+ ]
+
+ def initialize(project)
+ @project = project
+ end
+
+ def seed!
+ pipelines.each do |pipeline|
+ begin
+ BUILDS.each { |opts| build_create!(pipeline, opts) }
+ commit_status_create!(pipeline, name: 'jenkins', stage: 'test', status: :success)
+ print '.'
+ rescue ActiveRecord::RecordInvalid
+ print 'F'
+ ensure
+ pipeline.build_updated
+ end
+ end
+ end
+
+ private
+
+ def pipelines
+ create_master_pipelines + create_merge_request_pipelines
+ end
+
+ def create_master_pipelines
+ @project.repository.commits('master', limit: 4).map do |commit|
+ create_pipeline!(@project, 'master', commit)
+ end
+ rescue
+ []
+ end
+
+ def create_merge_request_pipelines
+ pipelines = @project.merge_requests.first(3).map do |merge_request|
+ project = merge_request.source_project
+ branch = merge_request.source_branch
+
+ merge_request.commits.last(4).map do |commit|
+ create_pipeline!(project, branch, commit)
+ end
+ end
+
+ pipelines.flatten
+ rescue
+ []
+ end
+
+
+ def create_pipeline!(project, ref, commit)
+ project.pipelines.create(sha: commit.id, ref: ref)
+ end
+
+ def build_create!(pipeline, opts = {})
+ attributes = job_attributes(pipeline, opts)
+ .merge(commands: '$ build command')
+
+ Ci::Build.create!(attributes).tap do |build|
+ # We need to set build trace and artifacts after saving a build
+ # (id required), that is why we need `#tap` method instead of passing
+ # block directly to `Ci::Build#create!`.
+
+ setup_artifacts(build)
+ setup_build_log(build)
+ build.save
+ end
+ end
+
+ def setup_artifacts(build)
+ return unless %w[build test].include?(build.stage)
+
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
+
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+ end
+
+ def setup_build_log(build)
+ if %w(running success failed).include?(build.status)
+ build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
+ end
+ end
+
+ def commit_status_create!(pipeline, opts = {})
+ attributes = job_attributes(pipeline, opts)
+
+ GenericCommitStatus.create!(attributes)
+ end
+
+ 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,
+ created_at: Time.now, updated_at: Time.now
+ }.merge(opts)
+ end
+
+ def build_user
+ @project.team.users.sample
+ end
+
+ def build_status
+ Ci::Build::AVAILABLE_STATUSES.sample
+ end
+
+ def stage_index(stage)
+ STAGES.index(stage) || 0
+ end
+
+ def artifacts_archive_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ end
+
+ def artifacts_metadata_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ end
+
+ def artifacts_cache_file(file_path)
+ cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
+
+ FileUtils.copy(file_path, cache_path)
+ File.open(cache_path) do |file|
+ yield file
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ Project.all.sample(5).each do |project|
+ project_builds = Gitlab::Seeder::Pipelines.new(project)
+ project_builds.seed!
+ end
+end
diff --git a/db/fixtures/development/16_protected_branches.rb b/db/fixtures/development/16_protected_branches.rb
new file mode 100644
index 00000000000..103c7f9445c
--- /dev/null
+++ b/db/fixtures/development/16_protected_branches.rb
@@ -0,0 +1,12 @@
+Gitlab::Seeder.quiet do
+ admin_user = User.find(1)
+
+ Project.all.each do |project|
+ params = {
+ name: 'master'
+ }
+
+ ProtectedBranches::CreateService.new(project, admin_user, params).execute
+ print '.'
+ end
+end
diff --git a/db/migrate/20140407135544_fix_namespaces.rb b/db/migrate/20140407135544_fix_namespaces.rb
index 91374966698..0026ce645a6 100644
--- a/db/migrate/20140407135544_fix_namespaces.rb
+++ b/db/migrate/20140407135544_fix_namespaces.rb
@@ -1,8 +1,14 @@
# rubocop:disable all
class FixNamespaces < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
- Namespace.where('name <> path and type is null').each do |namespace|
- namespace.update_attribute(:name, namespace.path)
+ namespaces = exec_query('SELECT id, path FROM namespaces WHERE name <> path and type is null')
+
+ namespaces.each do |row|
+ id = row['id']
+ path = row['path']
+ exec_query("UPDATE namespaces SET name = '#{path}' WHERE id = #{id}")
end
end
diff --git a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
index ac7eac0ea7c..611767ac7fe 100644
--- a/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
+++ b/db/migrate/20160302152808_remove_wrong_import_url_from_projects.rb
@@ -7,7 +7,13 @@ class RemoveWrongImportUrlFromProjects < ActiveRecord::Migration
class ProjectImportDataFake
extend AttrEncrypted
attr_accessor :credentials
- attr_encrypted :credentials, key: Gitlab::Application.secrets.db_key_base, marshal: true, encode: true, :mode => :per_attribute_iv_and_salt
+ attr_encrypted :credentials,
+ key: Gitlab::Application.secrets.db_key_base,
+ marshal: true,
+ encode: true,
+ :mode => :per_attribute_iv_and_salt,
+ insecure_mode: true,
+ algorithm: 'aes-256-cbc'
end
def up
diff --git a/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
new file mode 100644
index 00000000000..15ad8e8bcbb
--- /dev/null
+++ b/db/migrate/20160519203051_add_developers_can_merge_to_protected_branches.rb
@@ -0,0 +1,9 @@
+class AddDevelopersCanMergeToProtectedBranches < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_column_with_default :protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false
+ end
+end
diff --git a/db/migrate/20160629025435_add_column_in_progress_merge_commit_sha_to_merge_requests.rb b/db/migrate/20160629025435_add_column_in_progress_merge_commit_sha_to_merge_requests.rb
new file mode 100644
index 00000000000..7c5f76572ef
--- /dev/null
+++ b/db/migrate/20160629025435_add_column_in_progress_merge_commit_sha_to_merge_requests.rb
@@ -0,0 +1,8 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddColumnInProgressMergeCommitShaToMergeRequests < ActiveRecord::Migration
+ def change
+ add_column :merge_requests, :in_progress_merge_commit_sha, :string
+ end
+end
diff --git a/db/migrate/20160705054938_add_protected_branches_push_access.rb b/db/migrate/20160705054938_add_protected_branches_push_access.rb
new file mode 100644
index 00000000000..f27295524e1
--- /dev/null
+++ b/db/migrate/20160705054938_add_protected_branches_push_access.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddProtectedBranchesPushAccess < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :protected_branch_push_access_levels do |t|
+ t.references :protected_branch, index: { name: "index_protected_branch_push_access" }, foreign_key: true, null: false
+
+ # Gitlab::Access::MASTER == 40
+ t.integer :access_level, default: 40, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160705054952_add_protected_branches_merge_access.rb b/db/migrate/20160705054952_add_protected_branches_merge_access.rb
new file mode 100644
index 00000000000..32adfa266cd
--- /dev/null
+++ b/db/migrate/20160705054952_add_protected_branches_merge_access.rb
@@ -0,0 +1,17 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddProtectedBranchesMergeAccess < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :protected_branch_merge_access_levels do |t|
+ t.references :protected_branch, index: { name: "index_protected_branch_merge_access" }, foreign_key: true, null: false
+
+ # Gitlab::Access::MASTER == 40
+ t.integer :access_level, default: 40, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.rb
new file mode 100644
index 00000000000..1db0df92bec
--- /dev/null
+++ b/db/migrate/20160705055254_move_from_developers_can_merge_to_protected_branches_merge_access.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 MoveFromDevelopersCanMergeToProtectedBranchesMergeAccess < ActiveRecord::Migration
+ DOWNTIME = true
+ DOWNTIME_REASON = <<-HEREDOC
+ We're creating a `merge_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this
+ is running, we might be left with a `protected_branch` _without_ an associated `merge_access_level`. The `protected_branches`
+ table must not change while this is running, so downtime is required.
+
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410
+ HEREDOC
+
+ def up
+ execute <<-HEREDOC
+ INSERT into protected_branch_merge_access_levels (protected_branch_id, access_level, created_at, updated_at)
+ SELECT id, (CASE WHEN developers_can_merge THEN 30 ELSE 40 END), now(), now()
+ FROM protected_branches
+ HEREDOC
+ end
+
+ def down
+ execute <<-HEREDOC
+ UPDATE protected_branches SET developers_can_merge = TRUE
+ WHERE id IN (SELECT protected_branch_id FROM protected_branch_merge_access_levels
+ WHERE access_level = 30);
+ HEREDOC
+ end
+end
diff --git a/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.rb
new file mode 100644
index 00000000000..5c3e189bb5b
--- /dev/null
+++ b/db/migrate/20160705055308_move_from_developers_can_push_to_protected_branches_push_access.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 MoveFromDevelopersCanPushToProtectedBranchesPushAccess < ActiveRecord::Migration
+ DOWNTIME = true
+ DOWNTIME_REASON = <<-HEREDOC
+ We're creating a `push_access_level` for each `protected_branch`. If a user creates a `protected_branch` while this
+ is running, we might be left with a `protected_branch` _without_ an associated `push_access_level`. The `protected_branches`
+ table must not change while this is running, so downtime is required.
+
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081#note_13247410
+ HEREDOC
+
+ def up
+ execute <<-HEREDOC
+ INSERT into protected_branch_push_access_levels (protected_branch_id, access_level, created_at, updated_at)
+ SELECT id, (CASE WHEN developers_can_push THEN 30 ELSE 40 END), now(), now()
+ FROM protected_branches
+ HEREDOC
+ end
+
+ def down
+ execute <<-HEREDOC
+ UPDATE protected_branches SET developers_can_push = TRUE
+ WHERE id IN (SELECT protected_branch_id FROM protected_branch_push_access_levels
+ WHERE access_level = 30);
+ HEREDOC
+ end
+end
diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
new file mode 100644
index 00000000000..52a9819c628
--- /dev/null
+++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDevelopersCanPushFromProtectedBranches < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # This is only required for `#down`
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ remove_column :protected_branches, :developers_can_push, :boolean
+ end
+
+ def down
+ add_column_with_default(:protected_branches, :developers_can_push, :boolean, default: false, allow_null: false)
+ end
+end
diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
new file mode 100644
index 00000000000..4a7bde7f9f3
--- /dev/null
+++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveDevelopersCanMergeFromProtectedBranches < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # This is only required for `#down`
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ remove_column :protected_branches, :developers_can_merge, :boolean
+ end
+
+ def down
+ add_column_with_default(:protected_branches, :developers_can_merge, :boolean, default: false, allow_null: false)
+ end
+end
diff --git a/db/migrate/20160707104333_add_lock_to_issuables.rb b/db/migrate/20160707104333_add_lock_to_issuables.rb
new file mode 100644
index 00000000000..54866d02cbc
--- /dev/null
+++ b/db/migrate/20160707104333_add_lock_to_issuables.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 AddLockToIssuables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :issues, :lock_version, :integer
+ add_column :merge_requests, :lock_version, :integer
+ end
+
+ def down
+ remove_column :issues, :lock_version
+ remove_column :merge_requests, :lock_version
+ end
+end
diff --git a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
new file mode 100644
index 00000000000..668c22bb51c
--- /dev/null
+++ b/db/migrate/20160712171823_remove_award_emojis_with_no_user.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 RemoveAwardEmojisWithNoUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def up
+ AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all
+ end
+end
diff --git a/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.rb
new file mode 100644
index 00000000000..ecdd1bd7e5e
--- /dev/null
+++ b/db/migrate/20160713205315_add_domain_blacklist_to_application_settings.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 AddDomainBlacklistToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # 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, :domain_blacklist_enabled, :boolean, default: false
+ add_column :application_settings, :domain_blacklist, :text
+ end
+end
diff --git a/db/migrate/20160715132507_add_user_id_to_pipeline.rb b/db/migrate/20160715132507_add_user_id_to_pipeline.rb
new file mode 100644
index 00000000000..af0461c4daf
--- /dev/null
+++ b/db/migrate/20160715132507_add_user_id_to_pipeline.rb
@@ -0,0 +1,7 @@
+class AddUserIdToPipeline < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :ci_commits, :user_id, :integer
+ end
+end
diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
new file mode 100644
index 00000000000..7c991c6d998
--- /dev/null
+++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
@@ -0,0 +1,9 @@
+class AddIndexForPipelineUserId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_commits, :user_id
+ end
+end
diff --git a/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
new file mode 100644
index 00000000000..bf0131c6d76
--- /dev/null
+++ b/db/migrate/20160715154212_add_request_access_enabled_to_projects.rb
@@ -0,0 +1,12 @@
+class AddRequestAccessEnabledToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :projects, :request_access_enabled, :boolean, default: true
+ end
+
+ def down
+ remove_column :projects, :request_access_enabled
+ end
+end
diff --git a/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
new file mode 100644
index 00000000000..e7b14cd3ee2
--- /dev/null
+++ b/db/migrate/20160715204316_add_request_access_enabled_to_groups.rb
@@ -0,0 +1,12 @@
+class AddRequestAccessEnabledToGroups < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :namespaces, :request_access_enabled, :boolean, default: true
+ end
+
+ def down
+ remove_column :namespaces, :request_access_enabled
+ end
+end
diff --git a/db/migrate/20160715230841_rename_application_settings_restricted_signup_domains.rb b/db/migrate/20160715230841_rename_application_settings_restricted_signup_domains.rb
new file mode 100644
index 00000000000..dd15704800a
--- /dev/null
+++ b/db/migrate/20160715230841_rename_application_settings_restricted_signup_domains.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 RenameApplicationSettingsRestrictedSignupDomains < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # 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
+ rename_column :application_settings, :restricted_signup_domains, :domain_whitelist
+ end
+end
diff --git a/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb b/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb
new file mode 100644
index 00000000000..3e084023a65
--- /dev/null
+++ b/db/migrate/20160716115710_add_when_and_yaml_variables_to_ci_builds.rb
@@ -0,0 +1,8 @@
+class AddWhenAndYamlVariablesToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :ci_builds, :when, :string
+ add_column :ci_builds, :yaml_variables, :text
+ end
+end
diff --git a/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
new file mode 100644
index 00000000000..756910a1fa0
--- /dev/null
+++ b/db/migrate/20160716115711_add_queued_at_to_ci_builds.rb
@@ -0,0 +1,9 @@
+class AddQueuedAtToCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :queued_at, :timestamp
+ end
+end
diff --git a/db/migrate/20160718153603_add_has_external_wiki_to_projects.rb b/db/migrate/20160718153603_add_has_external_wiki_to_projects.rb
new file mode 100644
index 00000000000..55a3e954292
--- /dev/null
+++ b/db/migrate/20160718153603_add_has_external_wiki_to_projects.rb
@@ -0,0 +1,7 @@
+class AddHasExternalWikiToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :projects, :has_external_wiki, :boolean
+ end
+end
diff --git a/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
new file mode 100644
index 00000000000..1eb99feb40c
--- /dev/null
+++ b/db/migrate/20160721081015_drop_and_readd_has_external_wiki_in_projects.rb
@@ -0,0 +1,15 @@
+class DropAndReaddHasExternalWikiInProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:projects, :has_external_wiki, nil) do |table, query|
+ query.where(table[:has_external_wiki].not_eq(nil))
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160722221922_nullify_blank_type_on_notes.rb b/db/migrate/20160722221922_nullify_blank_type_on_notes.rb
new file mode 100644
index 00000000000..c4b78e8e15c
--- /dev/null
+++ b/db/migrate/20160722221922_nullify_blank_type_on_notes.rb
@@ -0,0 +1,9 @@
+class NullifyBlankTypeOnNotes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute "UPDATE notes SET type = NULL WHERE type = ''"
+ end
+end
diff --git a/db/migrate/20160724205507_add_resolved_to_notes.rb b/db/migrate/20160724205507_add_resolved_to_notes.rb
new file mode 100644
index 00000000000..b8ebcdbd156
--- /dev/null
+++ b/db/migrate/20160724205507_add_resolved_to_notes.rb
@@ -0,0 +1,10 @@
+class AddResolvedToNotes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notes, :resolved_at, :datetime
+ add_column :notes, :resolved_by_id, :integer
+ end
+end
diff --git a/db/migrate/20160725083350_add_external_url_to_enviroments.rb b/db/migrate/20160725083350_add_external_url_to_enviroments.rb
new file mode 100644
index 00000000000..21a8abd310b
--- /dev/null
+++ b/db/migrate/20160725083350_add_external_url_to_enviroments.rb
@@ -0,0 +1,9 @@
+class AddExternalUrlToEnviroments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column(:environments, :external_url, :string)
+ end
+end
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
new file mode 100644
index 00000000000..c8cbd2718ff
--- /dev/null
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.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 MergeRequestDiffRemoveUniq < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+
+ def down
+ unless index_exists?(:merge_request_diffs, :merge_request_id)
+ add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb
new file mode 100644
index 00000000000..6d04242dd25
--- /dev/null
+++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb
@@ -0,0 +1,17 @@
+class MergeRequestDiffAddIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index :merge_request_diffs, :merge_request_id
+ end
+
+ def down
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+end
diff --git a/db/migrate/20160727163552_create_user_agent_details.rb b/db/migrate/20160727163552_create_user_agent_details.rb
new file mode 100644
index 00000000000..ed4ccfedc0a
--- /dev/null
+++ b/db/migrate/20160727163552_create_user_agent_details.rb
@@ -0,0 +1,18 @@
+class CreateUserAgentDetails < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :user_agent_details do |t|
+ t.string :user_agent, null: false
+ t.string :ip_address, null: false
+ t.integer :subject_id, null: false
+ t.string :subject_type, null: false
+ t.boolean :submitted, default: false, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160727191041_create_boards.rb b/db/migrate/20160727191041_create_boards.rb
new file mode 100644
index 00000000000..56afbd4e030
--- /dev/null
+++ b/db/migrate/20160727191041_create_boards.rb
@@ -0,0 +1,13 @@
+class CreateBoards < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :boards do |t|
+ t.references :project, index: true, foreign_key: true, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160727193336_create_lists.rb b/db/migrate/20160727193336_create_lists.rb
new file mode 100644
index 00000000000..61d501215f2
--- /dev/null
+++ b/db/migrate/20160727193336_create_lists.rb
@@ -0,0 +1,16 @@
+class CreateLists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table :lists do |t|
+ t.references :board, index: true, foreign_key: true, null: false
+ t.references :label, index: true, foreign_key: true
+ t.integer :list_type, null: false, default: 1
+ t.integer :position
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb b/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb
new file mode 100644
index 00000000000..b800e6d7283
--- /dev/null
+++ b/db/migrate/20160728081025_add_pipeline_events_to_web_hooks.rb
@@ -0,0 +1,16 @@
+class AddPipelineEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:web_hooks, :pipeline_events, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:web_hooks, :pipeline_events)
+ end
+end
diff --git a/db/migrate/20160728103734_add_pipeline_events_to_services.rb b/db/migrate/20160728103734_add_pipeline_events_to_services.rb
new file mode 100644
index 00000000000..bcd24fe1566
--- /dev/null
+++ b/db/migrate/20160728103734_add_pipeline_events_to_services.rb
@@ -0,0 +1,16 @@
+class AddPipelineEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:services, :pipeline_events, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:services, :pipeline_events)
+ end
+end
diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
new file mode 100644
index 00000000000..e28ab31d629
--- /dev/null
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.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 RemoveProjectIdFromSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # 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 = 'Removing a column that contains data that is not used anywhere.'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ remove_column :spam_logs, :project_id, :integer
+ end
+end
diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb
new file mode 100644
index 00000000000..8db0fc60c4b
--- /dev/null
+++ b/db/migrate/20160801163421_add_expires_at_to_member.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 AddExpiresAtToMember < 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 :members, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
new file mode 100644
index 00000000000..296f1dfac7b
--- /dev/null
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.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 AddSubmittedAsHamToSpamLogs < 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 = ''
+
+ disable_ddl_transaction!
+
+ def change
+ add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
new file mode 100644
index 00000000000..5fd51cb65f1
--- /dev/null
+++ b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
@@ -0,0 +1,9 @@
+class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ remove_index :projects, column: :builds_enabled if index_exists?(:projects, :builds_enabled)
+ end
+end
diff --git a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
new file mode 100644
index 00000000000..baf2e70b127
--- /dev/null
+++ b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
@@ -0,0 +1,15 @@
+class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :lists, [:board_id, :label_id], unique: true
+ end
+
+ def down
+ remove_index :lists, column: [:board_id, :label_id] if index_exists?(:lists, [:board_id, :label_id], unique: true)
+ end
+end
diff --git a/db/migrate/20160804150737_add_timestamps_to_members_again.rb b/db/migrate/20160804150737_add_timestamps_to_members_again.rb
new file mode 100644
index 00000000000..6691ba57fbb
--- /dev/null
+++ b/db/migrate/20160804150737_add_timestamps_to_members_again.rb
@@ -0,0 +1,21 @@
+# rubocop:disable all
+# 20141121133009_add_timestamps_to_members.rb was meant to ensure that all
+# rows in the members table had created_at and updated_at set, following an
+# error in a previous migration. This failed to set all rows in at least one
+# case: https://gitlab.com/gitlab-org/gitlab-ce/issues/20568
+#
+# Why this happened is lost in the mists of time, so repeat the SQL query
+# without speculation, just in case more than one person was affected.
+class AddTimestampsToMembersAgain < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ execute "UPDATE members SET created_at = NOW() WHERE created_at IS NULL"
+ execute "UPDATE members SET updated_at = NOW() WHERE updated_at IS NULL"
+ end
+
+ def down
+ # no change
+ end
+
+end
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
new file mode 100644
index 00000000000..a853de3abfb
--- /dev/null
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -0,0 +1,12 @@
+class AddDeletedAtToNamespaces < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_column :namespaces, :deleted_at, :datetime
+ add_concurrent_index :namespaces, :deleted_at
+ end
+end
diff --git a/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
new file mode 100644
index 00000000000..0cfb637804b
--- /dev/null
+++ b/db/migrate/20160810102349_remove_ci_runner_trigram_indexes.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveCiRunnerTrigramIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # Disabled for the "down" method so the indexes can be re-created concurrently.
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ transaction do
+ execute 'DROP INDEX IF EXISTS index_ci_runners_on_token_trigram;'
+ execute 'DROP INDEX IF EXISTS index_ci_runners_on_description_trigram;'
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_token_trigram ON ci_runners USING gin(token gin_trgm_ops);'
+ execute 'CREATE INDEX CONCURRENTLY index_ci_runners_on_description_trigram ON ci_runners USING gin(description gin_trgm_ops);'
+ end
+end
diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb
new file mode 100644
index 00000000000..8641c6ffa8f
--- /dev/null
+++ b/db/migrate/20160810142633_remove_redundant_indexes.rb
@@ -0,0 +1,112 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveRedundantIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ indexes = [
+ [:ci_taggings, 'ci_taggings_idx'],
+ [:audit_events, 'index_audit_events_on_author_id'],
+ [:audit_events, 'index_audit_events_on_type'],
+ [:ci_builds, 'index_ci_builds_on_erased_by_id'],
+ [:ci_builds, 'index_ci_builds_on_project_id_and_commit_id'],
+ [:ci_builds, 'index_ci_builds_on_type'],
+ [:ci_commits, 'index_ci_commits_on_project_id'],
+ [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at'],
+ [:ci_commits, 'index_ci_commits_on_project_id_and_committed_at_and_id'],
+ [:ci_commits, 'index_ci_commits_on_project_id_and_sha'],
+ [:ci_commits, 'index_ci_commits_on_sha'],
+ [:ci_events, 'index_ci_events_on_created_at'],
+ [:ci_events, 'index_ci_events_on_is_admin'],
+ [:ci_events, 'index_ci_events_on_project_id'],
+ [:ci_jobs, 'index_ci_jobs_on_deleted_at'],
+ [:ci_jobs, 'index_ci_jobs_on_project_id'],
+ [:ci_projects, 'index_ci_projects_on_gitlab_id'],
+ [:ci_projects, 'index_ci_projects_on_shared_runners_enabled'],
+ [:ci_services, 'index_ci_services_on_project_id'],
+ [:ci_sessions, 'index_ci_sessions_on_session_id'],
+ [:ci_sessions, 'index_ci_sessions_on_updated_at'],
+ [:ci_tags, 'index_ci_tags_on_name'],
+ [:ci_triggers, 'index_ci_triggers_on_deleted_at'],
+ [:identities, 'index_identities_on_created_at_and_id'],
+ [:issues, 'index_issues_on_title'],
+ [:keys, 'index_keys_on_created_at_and_id'],
+ [:members, 'index_members_on_created_at_and_id'],
+ [:members, 'index_members_on_type'],
+ [:milestones, 'index_milestones_on_created_at_and_id'],
+ [:namespaces, 'index_namespaces_on_visibility_level'],
+ [:projects, 'index_projects_on_builds_enabled_and_shared_runners_enabled'],
+ [:services, 'index_services_on_category'],
+ [:services, 'index_services_on_created_at_and_id'],
+ [:services, 'index_services_on_default'],
+ [:snippets, 'index_snippets_on_created_at'],
+ [:snippets, 'index_snippets_on_created_at_and_id'],
+ [:todos, 'index_todos_on_state'],
+ [:web_hooks, 'index_web_hooks_on_created_at_and_id'],
+
+ # These indexes _may_ be used but they can be replaced by other existing
+ # indexes.
+
+ # There's already a composite index on (project_id, iid) which means that
+ # a separate index for _just_ project_id is not needed.
+ [:issues, 'index_issues_on_project_id'],
+
+ # These are all composite indexes for the columns (created_at, id). In all
+ # these cases there's already a standalone index for "created_at" which
+ # can be used instead.
+ #
+ # Because the "id" column of these composite indexes is never needed (due
+ # to "id" already being indexed as its a primary key) these composite
+ # indexes are useless.
+ [:issues, 'index_issues_on_created_at_and_id'],
+ [:merge_requests, 'index_merge_requests_on_created_at_and_id'],
+ [:namespaces, 'index_namespaces_on_created_at_and_id'],
+ [:notes, 'index_notes_on_created_at_and_id'],
+ [:projects, 'index_projects_on_created_at_and_id'],
+ [:users, 'index_users_on_created_at_and_id'],
+ ]
+
+ transaction do
+ indexes.each do |(table, index)|
+ remove_index(table, name: index) if index_exists_by_name?(table, index)
+ end
+ end
+
+ add_concurrent_index(:users, :created_at)
+ add_concurrent_index(:projects, :created_at)
+ add_concurrent_index(:namespaces, :created_at)
+ end
+
+ def down
+ # We're only restoring the composite indexes that could be replaced with
+ # individual ones, just in case somebody would ever want to revert.
+ transaction do
+ remove_index(:users, :created_at)
+ remove_index(:projects, :created_at)
+ remove_index(:namespaces, :created_at)
+ end
+
+ [:issues, :merge_requests, :namespaces, :notes, :projects, :users].each do |table|
+ add_concurrent_index(table, [:created_at, :id],
+ name: "index_#{table}_on_created_at_and_id")
+ end
+ end
+
+ # Rails' index_exists? doesn't work when you only give it a table and index
+ # name. As such we have to use some extra code to check if an index exists for
+ # a given name.
+ def index_exists_by_name?(table, index)
+ indexes_for_table[table].include?(index)
+ end
+
+ def indexes_for_table
+ @indexes_for_table ||= Hash.new do |hash, table_name|
+ hash[table_name] = indexes(table_name).map(&:name)
+ end
+ end
+end
diff --git a/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.rb
new file mode 100644
index 00000000000..7152bd04331
--- /dev/null
+++ b/db/migrate/20160816161312_add_column_name_to_u2f_registrations.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 AddColumnNameToU2fRegistrations < 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 :u2f_registrations, :name, :string
+ end
+end
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
new file mode 100644
index 00000000000..915d3d78e40
--- /dev/null
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -0,0 +1,10 @@
+class AddKodingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :koding_enabled, :boolean
+ add_column :application_settings, :koding_url, :string
+ end
+end
diff --git a/db/migrate/20160817154936_add_discussion_ids_to_notes.rb b/db/migrate/20160817154936_add_discussion_ids_to_notes.rb
new file mode 100644
index 00000000000..61facce665a
--- /dev/null
+++ b/db/migrate/20160817154936_add_discussion_ids_to_notes.rb
@@ -0,0 +1,13 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDiscussionIdsToNotes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :notes, :discussion_id, :string
+ add_column :notes, :original_discussion_id, :string
+ end
+end
diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
new file mode 100644
index 00000000000..0ed538b0df8
--- /dev/null
+++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.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 AddExpiresAtToProjectGroupLinks < 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 :project_group_links, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
new file mode 100644
index 00000000000..b6e8bb18e7b
--- /dev/null
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_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 AddIndexToNoteDiscussionId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :notes, :discussion_id
+ end
+end
diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
new file mode 100644
index 00000000000..0c68cf01900
--- /dev/null
+++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.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 ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'"
+ end
+end
diff --git a/db/migrate/20160823081327_change_merge_error_to_text.rb b/db/migrate/20160823081327_change_merge_error_to_text.rb
new file mode 100644
index 00000000000..7920389cd83
--- /dev/null
+++ b/db/migrate/20160823081327_change_merge_error_to_text.rb
@@ -0,0 +1,10 @@
+class ChangeMergeErrorToText < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because it alters a column from varchar(255) to text.'
+
+ def change
+ change_column :merge_requests, :merge_error, :text, limit: 65535
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a5eea3a697c..5a105a91ad1 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: 20160705163108) do
+ActiveRecord::Schema.define(version: 20160823081327) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -49,7 +49,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility"
t.integer "default_snippet_visibility"
- t.text "restricted_signup_domains"
+ t.text "domain_whitelist"
t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path"
t.integer "session_expire_delay", default: 10080, null: false
@@ -84,10 +84,14 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "health_check_access_token"
t.boolean "send_user_confirmation_email", default: false
t.integer "container_registry_token_expire_delay", default: 5
- t.boolean "user_default_external", default: false, null: false
t.text "after_sign_up_text"
+ t.boolean "user_default_external", default: false, null: false
t.string "repository_storage", default: "default"
t.string "enabled_git_access_protocol"
+ t.boolean "domain_blacklist_enabled", default: false
+ t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
end
create_table "audit_events", force: :cascade do |t|
@@ -100,9 +104,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "updated_at"
end
- add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
- add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "award_emoji", force: :cascade do |t|
t.string "name"
@@ -117,6 +119,14 @@ ActiveRecord::Schema.define(version: 20160705163108) do
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
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
+
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at"
@@ -165,9 +175,12 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
- t.string "environment"
t.datetime "artifacts_expire_at"
+ t.string "environment"
t.integer "artifacts_size"
+ t.string "when"
+ t.text "yaml_variables"
+ t.datetime "queued_at"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -175,13 +188,10 @@ ActiveRecord::Schema.define(version: 20160705163108) do
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", ["erased_by_id"], name: "index_ci_builds_on_erased_by_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", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
- add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
@@ -199,17 +209,14 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "started_at"
t.datetime "finished_at"
t.integer "duration"
+ t.integer "user_id"
end
add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
add_index "ci_commits", ["gl_project_id", "status"], name: "index_ci_commits_on_gl_project_id_and_status", using: :btree
add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
- add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
- add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
- add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
- add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
- add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
+ add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
create_table "ci_events", force: :cascade do |t|
t.integer "project_id"
@@ -220,10 +227,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "updated_at"
end
- add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
- add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
- add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
-
create_table "ci_jobs", force: :cascade do |t|
t.integer "project_id", null: false
t.text "commands"
@@ -238,9 +241,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "deleted_at"
end
- add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
- add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
-
create_table "ci_projects", force: :cascade do |t|
t.string "name"
t.integer "timeout", default: 3600, null: false
@@ -264,9 +264,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.text "generated_yaml_config"
end
- add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
- add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
-
create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
t.integer "project_id"
@@ -295,10 +292,8 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.boolean "locked", default: false, null: false
end
- add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
create_table "ci_services", force: :cascade do |t|
t.string "type"
@@ -310,8 +305,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.text "properties"
end
- add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
-
create_table "ci_sessions", force: :cascade do |t|
t.string "session_id", null: false
t.text "data"
@@ -319,9 +312,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "updated_at"
end
- add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
- add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
-
create_table "ci_taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -332,7 +322,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "created_at"
end
- add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "ci_tags", force: :cascade do |t|
@@ -340,8 +329,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "taggings_count", default: 0
end
- add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
-
create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
@@ -359,7 +346,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "gl_project_id"
end
- add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
@@ -421,9 +407,10 @@ ActiveRecord::Schema.define(version: 20160705163108) do
create_table "environments", force: :cascade do |t|
t.integer "project_id"
- t.string "name", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
+ t.string "external_url"
end
add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
@@ -464,7 +451,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "updated_at"
end
- add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
create_table "issues", force: :cascade do |t|
@@ -485,21 +471,19 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
+ t.integer "lock_version"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree
- add_index "issues", ["created_at", "id"], name: "index_issues_on_created_at_and_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
- add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
- add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "keys", force: :cascade do |t|
@@ -513,7 +497,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.boolean "public", default: false, null: false
end
- add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
@@ -561,6 +544,19 @@ ActiveRecord::Schema.define(version: 20160705163108) do
add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
+ create_table "lists", force: :cascade do |t|
+ t.integer "board_id", null: false
+ t.integer "label_id"
+ t.integer "list_type", default: 1, null: false
+ t.integer "position"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ 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|
t.integer "access_level", null: false
t.integer "source_id", null: false
@@ -575,14 +571,13 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
- add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
add_index "members", ["requested_at"], name: "index_members_on_requested_at", using: :btree
add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
- add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diffs", force: :cascade do |t|
@@ -598,12 +593,12 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "start_commit_sha"
end
- add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "source_project_id", null: false
+ t.string "target_branch", null: false
+ t.string "source_branch", null: false
+ t.integer "source_project_id", null: false
t.integer "author_id"
t.integer "assignee_id"
t.string "title"
@@ -612,23 +607,24 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "milestone_id"
t.string "state"
t.string "merge_status"
- t.integer "target_project_id", null: false
+ t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
- t.integer "position", default: 0
+ t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
- t.string "merge_error"
+ t.text "merge_error"
t.text "merge_params"
- t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
t.string "merge_commit_sha"
t.datetime "deleted_at"
+ t.string "in_progress_merge_commit_sha"
+ t.integer "lock_version"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
- add_index "merge_requests", ["created_at", "id"], name: "index_merge_requests_on_created_at_and_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
add_index "merge_requests", ["deleted_at"], name: "index_merge_requests_on_deleted_at", using: :btree
add_index "merge_requests", ["description"], name: "index_merge_requests_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -651,7 +647,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "iid"
end
- add_index "milestones", ["created_at", "id"], name: "index_milestones_on_created_at_and_id", using: :btree
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
@@ -660,26 +655,28 @@ ActiveRecord::Schema.define(version: 20160705163108) do
add_index "milestones", ["title"], name: "index_milestones_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
- t.boolean "share_with_group_lock", default: false
- t.integer "visibility_level", default: 20, null: false
+ 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.datetime "deleted_at"
end
- add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
+ 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"], 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", ["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
- add_index "namespaces", ["visibility_level"], name: "index_namespaces_on_visibility_level", using: :btree
create_table "notes", force: :cascade do |t|
t.text "note"
@@ -692,18 +689,22 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "line_code"
t.string "commit_id"
t.integer "noteable_id"
- t.boolean "system", default: false, null: false
+ t.boolean "system", default: false, null: false
t.text "st_diff"
t.integer "updated_by_id"
t.string "type"
t.text "position"
t.text "original_position"
+ t.datetime "resolved_at"
+ t.integer "resolved_by_id"
+ t.string "discussion_id"
+ t.string "original_discussion_id"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
- add_index "notes", ["created_at", "id"], name: "index_notes_on_created_at_and_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
@@ -773,10 +774,10 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "user_id", null: false
t.string "token", null: false
t.string "name", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.boolean "revoked", default: false
t.datetime "expires_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
@@ -788,6 +789,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
@@ -838,12 +840,12 @@ ActiveRecord::Schema.define(version: 20160705163108) 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 "has_external_wiki"
end
- add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
- add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
- add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
+ add_index "projects", ["created_at"], name: "index_projects_on_created_at", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["description"], name: "index_projects_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
@@ -857,12 +859,29 @@ ActiveRecord::Schema.define(version: 20160705163108) do
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
+ create_table "protected_branch_merge_access_levels", force: :cascade do |t|
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "protected_branch_merge_access_levels", ["protected_branch_id"], name: "index_protected_branch_merge_access", using: :btree
+
+ create_table "protected_branch_push_access_levels", force: :cascade do |t|
+ t.integer "protected_branch_id", null: false
+ t.integer "access_level", default: 40, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "protected_branch_push_access_levels", ["protected_branch_id"], name: "index_protected_branch_push_access", using: :btree
+
create_table "protected_branches", force: :cascade do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
+ t.integer "project_id", null: false
+ t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "developers_can_push", default: false, null: false
end
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
@@ -910,11 +929,9 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "category", default: "common", null: false
t.boolean "default", default: false
t.boolean "wiki_page_events", default: true
+ t.boolean "pipeline_events", default: false, null: false
end
- add_index "services", ["category"], name: "index_services_on_category", using: :btree
- add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
- add_index "services", ["default"], name: "index_services_on_default", using: :btree
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
add_index "services", ["template"], name: "index_services_on_template", using: :btree
@@ -931,8 +948,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
- add_index "snippets", ["created_at", "id"], name: "index_snippets_on_created_at_and_id", using: :btree
- add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["file_name"], name: "index_snippets_on_file_name_trigram", using: :gin, opclasses: {"file_name"=>"gin_trgm_ops"}
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["title"], name: "index_snippets_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -944,12 +959,12 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.string "source_ip"
t.string "user_agent"
t.boolean "via_api"
- t.integer "project_id"
t.string "noteable_type"
t.string "title"
t.text "description"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.boolean "submitted_as_ham", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -1001,7 +1016,6 @@ ActiveRecord::Schema.define(version: 20160705163108) do
add_index "todos", ["commit_id"], name: "index_todos_on_commit_id", using: :btree
add_index "todos", ["note_id"], name: "index_todos_on_note_id", using: :btree
add_index "todos", ["project_id"], name: "index_todos_on_project_id", using: :btree
- add_index "todos", ["state"], name: "index_todos_on_state", using: :btree
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
@@ -1013,11 +1027,22 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.string "name"
end
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
+ create_table "user_agent_details", force: :cascade do |t|
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
+ t.boolean "submitted", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -1081,7 +1106,7 @@ ActiveRecord::Schema.define(version: 20160705163108) do
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
- add_index "users", ["created_at", "id"], name: "index_users_on_created_at_and_id", using: :btree
+ add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
@@ -1119,11 +1144,16 @@ ActiveRecord::Schema.define(version: 20160705163108) do
t.boolean "build_events", default: false, null: false
t.boolean "wiki_page_events", default: false, null: false
t.string "token"
+ t.boolean "pipeline_events", default: false, null: false
end
- add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
+ add_foreign_key "boards", "projects"
+ add_foreign_key "lists", "boards"
+ add_foreign_key "lists", "labels"
add_foreign_key "personal_access_tokens", "users"
+ add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
+ add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index cf7a828d91e..254394eb63e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -2,6 +2,7 @@
## User documentation
+- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
@@ -9,9 +10,9 @@
- [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 and exporting projects between instances](user/project/settings/import_export.md).
-- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
+- [Markdown](user/markdown.md) GitLab's advanced formatting system.
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
-- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
+- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
@@ -21,7 +22,7 @@
## Administrator documentation
-- [Access restrictions](administration/access_restrictions.md) Define which Git access protocols can be used to talk to GitLab
+- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols) Define which Git access protocols can be used to talk to GitLab
- [Authentication/Authorization](administration/auth/README.md) Configure
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom Git hooks](administration/custom_hooks.md) Custom Git hooks (on the filesystem) for when webhooks aren't enough.
@@ -29,6 +30,7 @@
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
@@ -50,10 +52,9 @@
- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
+- [Multiple mountpoints for the repositories storage](administration/repository_storages.md) Define multiple repository storage paths to distribute the storage load.
## Contributor documentation
-- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are
- contributing to documentation.
-- [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
+- [Development](development/README.md) All styleguides and explanations how to contribute.
- [Legal](legal/README.md) Contributor license agreements.
diff --git a/doc/administration/access_restrictions.md b/doc/administration/access_restrictions.md
deleted file mode 100644
index 51d7996effd..00000000000
--- a/doc/administration/access_restrictions.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Access Restrictions
-
-> **Note:** This feature is only available on versions 8.10 and above.
-
-With GitLab's Access restrictions you can choose which Git access protocols you
-want your users to use to communicate with GitLab. This feature can be enabled
-via the `Application Settings` in the Admin interface.
-
-The setting is called `Enabled Git access protocols`, and it gives you the option
-to choose between:
-
-- Both SSH and HTTP(S)
-- Only SSH
-- Only HTTP(s)
-
-![Settings Overview](img/access_restrictions.png)
-
-## Enabled Protocol
-
-When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give
-your users the option to choose which protocol they would like to use.
-
-When you choose to allow only one of the protocols, a couple of things will happen:
-
-- The project page will only show the allowed protocol's URL, with no option to
- change it.
-- A tooltip will be shown when you hover over the URL's protocol, if an action
- on the user's part is required, e.g. adding an SSH key, or setting a password.
-
-![Project URL with SSH only access](img/restricted_url.png)
-
-On top of these UI restrictions, GitLab will deny all Git actions on the protocol
-not selected.
-
-> **Note:** Please keep in mind that disabling an access protocol does not actually
- block access to the server itself. The ports used for the protocol, be it SSH or
- HTTP, will still be accessible. What GitLab does is restrict access on the
- application level. \ No newline at end of file
diff --git a/doc/administration/build_artifacts.md b/doc/administration/build_artifacts.md
new file mode 100644
index 00000000000..64353f7282b
--- /dev/null
+++ b/doc/administration/build_artifacts.md
@@ -0,0 +1,90 @@
+# Build artifacts administration
+
+>**Notes:**
+>- Introduced in GitLab 8.2 and GitLab Runner 0.7.0.
+>- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+ changed to `ZIP`.
+>- This is the administration documentation. For the user guide see
+ [user/project/builds/artifacts.md](../user/project/builds/artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a build
+after it completes successfully. This feature is enabled by default in all
+GitLab installations. Keep reading if you want to know how to disable it.
+
+## Disabling build artifacts
+
+To disable artifacts site-wide, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_enabled'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: false
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## Storing build artifacts
+
+After a successful build, GitLab Runner uploads an archive containing the build
+artifacts to GitLab.
+
+To change the location where the artifacts are stored, follow the steps below.
+
+---
+
+**In Omnibus installations:**
+
+_The artifacts are stored by default in
+`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/etc/gitlab/gitlab.rb` and add the following line:
+
+ ```ruby
+ gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**In installations from source:**
+
+_The artifacts are stored by default in
+`/home/git/gitlab/shared/artifacts`._
+
+1. To change the storage path for example to `/mnt/storage/artifacts`, edit
+ `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
+
+ ```yaml
+ artifacts:
+ enabled: true
+ path: /mnt/storage/artifacts
+ ```
+
+1. Save the file and [restart GitLab][] for the changes to take effect.
+
+## 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).
+
+[reconfigure gitlab]: restart_gitlab.md "How to restart GitLab"
+[restart gitlab]: restart_gitlab.md "How to restart GitLab"
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index d5d43303454..28c4c7c86ca 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,7 +1,6 @@
# GitLab Container Registry Administration
-> **Note:**
-This feature was [introduced][ce-4040] in GitLab 8.8.
+> [Introduced][ce-4040] in GitLab 8.8.
With the Docker Container Registry integrated into GitLab, every project can
have its own space to store its Docker images.
@@ -122,6 +121,10 @@ Registry is exposed to the outside world is `4567`, here is what you need to set
in `gitlab.rb` or `gitlab.yml` if you are using Omnibus GitLab or installed
GitLab from source respectively.
+>**Note:**
+Be careful to choose a port different than the one that Registry listens to (`5000` by default),
+otherwise you will run into conflicts .
+
---
**Omnibus GitLab installations**
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index e3306c22d3f..0387d730489 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -44,8 +44,7 @@ as appropriate.
## Custom error messages
->**Note:**
-This feature was [introduced][5073] in GitLab 8.10.
+> [Introduced][5073] in GitLab 8.10.
If the commit is declined or an error occurs during the Git hook check,
the STDERR or STDOUT message of the hook will be present in GitLab's UI.
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index f6153216f33..bc424330656 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -1,7 +1,12 @@
# Configuring Redis for GitLab HA
-You can choose to install and manage Redis yourself, or you can use GitLab
-Omnibus packages to help.
+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
+ [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.
## Configure your own Redis server
@@ -9,49 +14,293 @@ 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.
-> **Note:** Redis does not require authentication by default. 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.
+## Configure Redis using Omnibus
-## Configure using Omnibus
+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.
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.
+ URL:
```ruby
- external_url 'https://gitlab.example.com'
+ external_url 'https://gitlab.example.com'
- # Disable all components 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
+ # 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'
+ # Redis configuration
+ redis['port'] = 6379
+ redis['bind'] = '0.0.0.0'
- # If you wish to use Redis authentication (recommended)
- redis['password'] = 'Redis Password'
+ # If you wish to use Redis authentication (recommended)
+ redis['password'] = 'Redis Password'
```
1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
> **Note**: This `reconfigure` step will result in some errors.
That's OK - don't be alarmed.
+
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.
+## Experimental Redis Sentinel support
+
+> [Introduced][ce-1877] in GitLab 8.11.
+
+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.
+
+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.
+
+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.
+
+The configuration consists of three parts:
+
+- Redis setup
+- Sentinel setup
+- GitLab setup
+
+Read carefully how to configure those components below.
+
+### Redis setup
+
+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.
+
+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.
+
+---
+
+**Configuring your own Redis server**
+
+1. Add to the slaves' `redis.conf`:
+
+ ```conf
+ # IP and port of the master Redis server
+ slaveof 10.10.10.10 6379
+ ```
+
+1. Optionally, set up password authentication for increased security.
+ Add the following to master's `redis.conf`:
+
+ ```conf
+ # Optional password authentication for increased security
+ requirepass "<password>"
+ ```
+
+1. Then add this line to all the slave servers' `redis.conf`:
+
+ ```conf
+ masterauth "<password>"
+ ```
+
+1. Restart the Redis services for the changes to take effect.
+
+---
+
+**Using Redis via Omnibus**
+
+1. Edit `/etc/gitlab/gitlab.rb` of a master Redis machine (usualy a single machine):
+
+ ```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
+
+ ## Master redis instance
+ redis['password'] = '<huge password string here>'
+ ```
+
+1. Edit `/etc/gitlab/gitlab.rb` of a slave Redis machine (should be one or more machines):
+
+ ```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
+
+ ## 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. Reconfigure the GitLab for the changes to take effect: `sudo gitlab-ctl reconfigure`
+
+---
+
+Now that the Redis servers are all set up, let's configure the Sentinel
+servers.
+
+### 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.
+
+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:
+
+```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.
+
+Here is an example configuration file (`sentinel.conf`) for a Sentinel node:
+
+```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
+```
+
+---
+
+The final part is to inform the main GitLab application server of the Redis
+master and the new sentinels servers.
+
+### GitLab setup
+
+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.
+
+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.
+
+>**Note:**
+The following steps should be performed in the [GitLab application server](gitlab.md).
+
+**For source based installations**
+
+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.
+
+**For Omnibus installations**
+
+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}
+ ]
+ ```
+
+1. [Reconfigure] the GitLab for the changes to take effect.
+
+### Sentinel troubleshooting
+
+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).
+
+It's a bit rigid the way you have to config `resque.yml` and `sentinel.conf`,
+otherwise `redis-rb` will not work properly.
+
+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:
+
+```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
+```
+
+```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
+```
+
+When in doubt, please read [Redis Sentinel documentation](http://redis.io/topics/sentinel)
+
+---
+
+To make sure your configuration is correct:
+
+1. SSH into your GitLab application server
+1. Enter the Rails console:
+
+ ```
+ # For Omnibus installations
+ sudo gitlab-rails console
+
+ # For source installations
+ sudo -u git rails console RAILS_ENV=production
+ ```
+
+1. Run in the console:
+
+ ```ruby
+ redis = Redis.new(Gitlab::Redis.params)
+ redis.info
+ ```
+
+ Keep this screen open and try to simulate a failover below.
+
+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
+ ```
+
+1. Then back in the Rails console from the first step, run:
+
+ ```
+ redis.info
+ ```
+
+ You should see a different port after a few seconds delay
+ (the failover/reconnect time).
+
---
Read more on high-availability configuration:
@@ -60,3 +309,9 @@ Read more on high-availability configuration:
1. [Configure NFS](nfs.md)
1. [Configure the GitLab application servers](gitlab.md)
1. [Configure the load balancers](load_balancer.md)
+
+[ce-1877]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1877
+[restart]: ../restart_gitlab.md#installations-from-source
+[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
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index a5fa7d358a2..34b4f1faa94 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -1,6 +1,6 @@
# Housekeeping
-_**Note:** This feature was [introduced][ce-2371] in GitLab 8.4_
+> [Introduced][ce-2371] in GitLab 8.4.
---
diff --git a/doc/administration/img/access_restrictions.png b/doc/administration/img/access_restrictions.png
deleted file mode 100644
index 66fd9491e85..00000000000
--- a/doc/administration/img/access_restrictions.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png
new file mode 100644
index 00000000000..599350bc098
--- /dev/null
+++ b/doc/administration/img/repository_storages_admin_ui.png
Binary files differ
diff --git a/doc/administration/img/restricted_url.png b/doc/administration/img/restricted_url.png
deleted file mode 100644
index 0a677433dcf..00000000000
--- a/doc/administration/img/restricted_url.png
+++ /dev/null
Binary files differ
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
new file mode 100644
index 00000000000..a2c358af095
--- /dev/null
+++ b/doc/administration/integration/koding.md
@@ -0,0 +1,242 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through installing and configuring Koding with
+GitLab.
+
+First of all, to be able to use Koding and GitLab together you will need public
+access to your server. This allows you to use single sign-on from GitLab to
+Koding and using vms from cloud providers like AWS. Koding has a registry for
+VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
+cloud providers register themselves to Kontrol via the agent that we put into
+provisioned VMs. This agent is called Klient and it provides Koding to access
+and manage the target machine.
+
+Kontrol and Klient are based on another technology called
+[Kite](https://github.com/koding/kite), that we have written at Koding. Which is a
+microservice framework that allows you to develop microservices easily.
+
+## Requirements
+
+### Hardware
+
+Minimum requirements are;
+
+ - 2 cores CPU
+ - 3G RAM
+ - 10G Storage
+
+If you plan to use AWS to install Koding it is recommended that you use at
+least a `c3.xlarge` instance.
+
+### Software
+
+ - [Git](https://git-scm.com)
+ - [Docker](https://www.docker.com)
+ - [docker-compose](https://www.docker.com/products/docker-compose)
+
+Koding can run on most of the UNIX based operating systems, since it's shipped
+as containerized with Docker support, it can work on any operating system that
+supports Docker.
+
+Required services are:
+
+- **PostgreSQL** - Kontrol and Service DB provider
+- **MongoDB** - Main DB provider the application
+- **Redis** - In memory DB used by both application and services
+- **RabbitMQ** - Message Queue for both application and services
+
+which are also provided as a Docker container by Koding.
+
+
+## Getting Started with Development Versions
+
+
+### Koding
+
+You can run `docker-compose` environment for developing koding by
+executing commands in the following snippet.
+
+```bash
+git clone https://github.com/koding/koding.git
+cd koding
+docker-compose up
+```
+
+This should start koding on `localhost:8090`.
+
+By default there is no team exists in Koding DB. You'll need to create a team
+called `gitlab` which is the default team name for GitLab integration in the
+configuration. To make things in order it's recommended to create the `gitlab`
+team first thing after setting up Koding.
+
+
+### GitLab
+
+To install GitLab to your environment for development purposes it's recommended
+to use GitLab Development Kit which you can get it from
+[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
+
+After all those steps, gitlab should be running on `localhost:3000`
+
+
+## Integration
+
+Integration includes following components;
+
+ - Single Sign On with OAuth from GitLab to Koding
+ - System Hook integration for handling GitLab events on Koding
+ (`project_created`, `user_joined` etc.)
+ - Service endpoints for importing/executing stacks from GitLab to Koding
+ (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
+
+As it's pointed out before, you will need public access to this machine that
+you've installed Koding and GitLab on. Better to use a domain but a static IP
+is also fine.
+
+For IP based installation you can use [xip.io](https://xip.io) service which is
+free and provides DNS resolution to IP based requests like following;
+
+ - 127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - and so on...
+
+As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+a running koding instance on `127.0.0.1` server will be handled as `foo` team
+requests.
+
+
+### GitLab Side
+
+You need to enable Koding integration from Settings under Admin Area. To do
+that login with an Admin account and do followings;
+
+ - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
+ - scroll to bottom of the page until Koding section
+ - check `Enable Koding` checkbox
+ - provide GitLab team page for running Koding instance as `Koding URL`*
+
+* For `Koding URL` you need to provide the gitlab integration enabled team on
+your Koding installation. Team called `gitlab` has integration on Koding out
+of the box, so if you didn't change anything your team on Koding should be
+`gitlab`.
+
+So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+with your Koding installation here.
+
+
+#### Registering Koding for OAuth integration
+
+We need `Application ID` and `Secret` to enable login to Koding via GitLab
+feature and to do that you need to register running Koding as a new application
+to your running GitLab application. Follow
+[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
+enable this integration.
+
+Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
+which again you need to _replace `127.0.0.1` with your instance public IP._
+
+Take a copy of `Application ID` and `Secret` that is generated by the GitLab
+application, we will need those on _Koding Part_ of this guide.
+
+
+#### Registering system hooks to Koding (optional)
+
+Koding can take actions based on the events generated by GitLab application.
+This feature is still in progress and only following events are processed by
+Koding at the moment;
+
+ - user_create
+ - user_destroy
+
+All system events are handled but not implemented on Koding side.
+
+To enable this feature you need to provide a `URL` and a `Secret Token` to your
+GitLab application. Open your admin area on your GitLab app from
+[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
+and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
+endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
+keep a copy of it, we will need it on _Koding Part_ of this guide.
+
+_(replace `127.0.0.1` with your instance public IP)_
+
+
+### Koding Part
+
+If you followed the steps in GitLab part we should have followings to enable
+Koding part integrations;
+
+ - `Application ID` and `Secret` for OAuth integration
+ - `Secret Token` for system hook integration
+ - Public address of running GitLab instance
+
+
+#### Start Koding with GitLab URL
+
+Now we need to configure Koding with all this information to get things ready.
+If it's already running please stop koding first.
+
+##### From command-line
+
+Replace followings with the ones you got from GitLab part of this guide;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+ --gitlabHost=**GITLAB_IP** \
+ --gitlabPort=**GITLAB_PORT** \
+ --gitlabToken=**SECRET_TOKEN** \
+ --gitlabAppId=**APPLICATION_ID** \
+ --gitlabAppSecret=**SECRET**
+```
+
+##### By updating configuration
+
+Alternatively you can update `gitlab` section on
+`config/credentials.default.coffee` like following;
+
+```
+gitlab =
+ host: '**GITLAB_IP**'
+ port: '**GITLAB_PORT**'
+ applicationId: '**APPLICATION_ID**'
+ applicationSecret: '**SECRET**'
+ team: 'gitlab'
+ redirectUri: ''
+ systemHookToken: '**SECRET_TOKEN**'
+ hooksEnabled: yes
+```
+
+and start by only providing the `host`;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+```
+
+#### Enable Single Sign On
+
+Once you restarted your Koding and logged in with your username and password
+you need to activate oauth authentication for your user. To do that
+
+ - Navigate to Dashboard on Koding from;
+ `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ - Scroll down to Integrations section
+ - Click on toggle to turn On integration in GitLab integration section
+
+This will redirect you to your GitLab instance and will ask your permission (
+if you are not logged in to GitLab at this point you will be redirected after
+login) once you accept you will be redirected to your Koding instance.
+
+From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
+screen in your Koding instance.
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index c212059b9d5..39b1883375e 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -1,13 +1,14 @@
# Project import/export
>**Note:**
- - This feature was [introduced][ce-3050] in GitLab 8.9
- - Importing will not be possible if the import instance version is lower
- than that of the exporter.
- - For existing installations, the project import option has to be enabled in
- application settings (`/admin/application_settings`) under 'Import sources'.
- - The exports are stored in a temporary [shared directory][tmp] and are deleted
- every 24 hours by a specific worker.
+>
+> - [Introduced][ce-3050] in GitLab 8.9.
+> - Importing will not be possible if the import instance version is lower
+> than that of the exporter.
+> - For existing installations, the project import option has to be enabled in
+> application settings (`/admin/application_settings`) under 'Import sources'.
+> - The exports are stored in a temporary [shared directory][tmp] and are deleted
+> every 24 hours by a specific worker.
The GitLab Import/Export version can be checked by using:
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index 4172b604cec..bc2b1f20ed3 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -1,8 +1,7 @@
# Repository checks
->**Note:**
-This feature was [introduced][ce-3232] in GitLab 8.7. It is OFF by
-default because it still causes too many false alarms.
+> [Introduced][ce-3232] in GitLab 8.7. It is OFF by default because it still
+causes too many false alarms.
Git has a built-in mechanism, [git fsck][git-fsck], to verify the
integrity of all data committed to a repository. GitLab administrators
diff --git a/doc/administration/repository_storages.md b/doc/administration/repository_storages.md
index 81bfe173151..55b054fc1a4 100644
--- a/doc/administration/repository_storages.md
+++ b/doc/administration/repository_storages.md
@@ -1,18 +1,99 @@
# Repository storages
-GitLab allows you to define repository storage paths to enable distribution of
-storage load between several mount points.
-
-## For installations from source
+> [Introduced][ce-4578] in GitLab 8.10.
-Add your repository storage paths in your `gitlab.yml` under repositories -> storages, using key -> value pairs.
+GitLab allows you to define multiple repository storage paths to distribute the
+storage load between several mount points.
>**Notes:**
+>
- You must have at least one storage path called `default`.
-- In order for backups to work correctly the storage path must **not** be a
+- The paths are defined in key-value pairs. The key is an arbitrary name you
+ can pick to name the file path.
+- The target directories and any of its subpaths must not be a symlink.
+
+## Configure GitLab
+
+>**Warning:**
+In order for [backups] to work correctly, the storage path must **not** be a
mount point and the GitLab user should have correct permissions for the parent
-directory of the path.
+directory of the path. In Omnibus GitLab this is taken care of automatically,
+but for source installations you should be extra careful.
+>
+The thing is that for compatibility reasons `gitlab.yml` has a different
+structure than Omnibus. In `gitlab.yml` you indicate the path for the
+repositories, for example `/home/git/repositories`, while in Omnibus you
+indicate `git_data_dirs`, which for the example above would be `/home/git`.
+Then, Omnibus will create a `repositories` directory under that path to use with
+`gitlab.yml`.
+>
+This little detail matters because while restoring a backup, the current
+contents of `/home/git/repositories` [are moved to][raketask] `/home/git/repositories.old`,
+so if `/home/git/repositories` is the mount point, then `mv` would be moving
+things between mount points, and bad things could happen. Ideally,
+`/home/git` would be the mount point, so then things would be moving within the
+same mount point. This is guaranteed with Omnibus installations (because they
+don't specify the full repository path but the parent path), but not for source
+installations.
+
+---
+
+Now that you've read that big fat warning above, let's edit the configuration
+files and add the full paths of the alternative repository storage paths. In
+the example below, we add two more mountpoints that are named `nfs` and `cephfs`
+respectively.
+
+**For installations from source**
+
+1. Edit `gitlab.yml` and add the storage paths:
+
+ ```yaml
+ repositories:
+ # Paths where repositories can be stored. Give the canonicalized absolute pathname.
+ # NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
+ storages: # You must have at least a 'default' storage path.
+ default: /home/git/repositories
+ nfs: /mnt/nfs/repositories
+ cephfs: /mnt/cephfs/repositories
+ ```
+
+1. [Restart GitLab] for the changes to take effect.
+
+>**Note:**
+The [`gitlab_shell: repos_path` entry][repospath] in `gitlab.yml` will be
+deprecated and replaced by `repositories: storages` in the future, so if you
+are upgrading from a version prior to 8.10, make sure to add the configuration
+as described in the step above. After you make the changes and confirm they are
+working, you can remove the `repos_path` line.
+
+---
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb` by appending the rest of the paths to the
+ default one:
+
+ ```ruby
+ git_data_dirs({
+ "default" => "/var/opt/gitlab/git-data",
+ "nfs" => "/mnt/nfs/git-data",
+ "cephfs" => "/mnt/cephfs/git-data"
+ })
+ ```
+
+ Note that Omnibus stores the repositories in a `repositories` subdirectory
+ of the `git-data` directory.
+
+## Choose where new project repositories will be stored
+
+Once you set the multiple storage paths, you can choose where new projects will
+be stored via the **Application Settings** in the Admin area.
-## For omnibus installations
+![Choose repository storage path in Admin area](img/repository_storages_admin_ui.png)
-Follow the instructions at https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/configuration.md#storing-git-data-in-an-alternative-directory
+[ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578
+[restart gitlab]: restart_gitlab.md#installations-from-source
+[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[backups]: ../raketasks/backup_restore.md
+[raketask]: https://gitlab.com/gitlab-org/gitlab-ce/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56
+[repospath]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/config/gitlab.yml.example#L457
diff --git a/doc/api/README.md b/doc/api/README.md
index d1e6c54c521..3e79cce0120 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -16,6 +16,8 @@ following locations:
- [Commits](commits.md)
- [Deploy Keys](deploy_keys.md)
- [Groups](groups.md)
+- [Group Access Requests](access_requests.md)
+- [Group Members](members.md)
- [Issues](issues.md)
- [Keys](keys.md)
- [Labels](labels.md)
@@ -24,7 +26,10 @@ following locations:
- [Open source license templates](licenses.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
+- [Pipelines](pipelines.md)
- [Projects](projects.md) including setting Webhooks
+- [Project Access Requests](access_requests.md)
+- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
@@ -74,14 +79,14 @@ You can use an OAuth 2 token to authenticate with the API by passing it either i
Example of using the OAuth2 token in the header:
```shell
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v3/projects
```
Read more about [GitLab as an OAuth2 client](oauth2.md).
### Personal Access Tokens
-> **Note:** This feature was [introduced][ce-3749] in GitLab 8.8
+> [Introduced][ce-3749] in GitLab 8.8.
You can create as many personal access tokens as you like from your GitLab
profile (`/profile/personal_access_tokens`); perhaps one for each application
@@ -154,7 +159,7 @@ be returned with status code `403`:
```json
{
- "message": "403 Forbidden: Must be admin to use sudo"
+ "message": "403 Forbidden - Must be admin to use sudo"
}
```
@@ -204,7 +209,7 @@ resources you can pass the following parameters:
In the example below, we list 50 [namespaces](namespaces.md) per page.
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/namespaces?per_page=50
```
### Pagination Link header
@@ -218,7 +223,7 @@ and we request the second page (`page=2`) of [comments](notes.md) of the issue
with ID `8` which belongs to the project with ID `8`:
```bash
-curl -I -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
+curl --head --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/8/issues/8/notes?per_page=3&page=2
```
The response will then be:
diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md
new file mode 100644
index 00000000000..ea308b54d62
--- /dev/null
+++ b/doc/api/access_requests.md
@@ -0,0 +1,147 @@
+# Group and project access requests
+
+ >**Note:** This feature was introduced in GitLab 8.11
+
+ **Valid access levels**
+
+ The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List access requests for a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "requested_at": "2012-10-22T14:13:35Z"
+ },
+ {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "requested_at": "2012-10-22T14:13:35Z"
+ }
+]
+```
+
+## Request access to a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "requested_at": "2012-10-22T14:13:35Z"
+}
+```
+
+## Approve an access request
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the access requester |
+| `access_level` | integer | no | A valid access level (defaults: `30`, developer access level) |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id/approve?access_level=20
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id/approve?access_level=20
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 20
+}
+```
+
+## Deny an access request
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the access requester |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/access_requests/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/access_requests/:user_id
+```
diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md
index b44f8cfd628..72ec99b7c56 100644
--- a/doc/api/award_emoji.md
+++ b/doc/api/award_emoji.md
@@ -1,6 +1,6 @@
# Award Emoji
- >**Note:** This feature was introduced in GitLab 8.9
+> [Introduced][ce-4575] in GitLab 8.9.
An awarded emoji tells a thousand words, and can be awarded on issues, merge
requests and notes/comments. Issues, merge requests and notes are further called
@@ -25,7 +25,7 @@ Parameters:
| `awardable_id` | integer | yes | The ID of an awardable |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji
```
Example Response:
@@ -67,9 +67,9 @@ Example Response:
]
```
-### Get single issue note
+### Get single award emoji
-Gets a single award emoji
+Gets a single award emoji from an issue or merge request.
```
GET /projects/:id/issues/:issue_id/award_emoji/:award_id
@@ -85,7 +85,7 @@ Parameters:
| `award_id` | integer | yes | The ID of the award emoji |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/1
```
Example Response:
@@ -127,7 +127,7 @@ Parameters:
| `name` | string | yes | The name of the emoji, without colons |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji?name=blowfish
```
Example Response:
@@ -170,7 +170,7 @@ Parameters:
| `award_id` | integer | yes | The ID of a award_emoji |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/344
```
Example Response:
@@ -217,7 +217,7 @@ Parameters:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji
```
Example Response:
@@ -259,7 +259,7 @@ Parameters:
| `award_id` | integer | yes | The ID of the award emoji |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji/2
```
Example Response:
@@ -299,7 +299,7 @@ Parameters:
| `name` | string | yes | The name of the emoji, without colons |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/notes/1/award_emoji?name=rocket
```
Example Response:
@@ -342,7 +342,7 @@ Parameters:
| `award_id` | integer | yes | The ID of a award_emoji |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" http://gitlab.example.com/api/v3/projects/1/issues/80/award_emoji/345
```
Example Response:
@@ -365,3 +365,5 @@ Example Response:
"awardable_type": "Note"
}
```
+
+[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575
diff --git a/doc/api/branches.md b/doc/api/branches.md
index abc4732c395..0b5f7778fc7 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -13,7 +13,7 @@ GET /projects/:id/repository/branches
| `id` | integer | yes | The ID of a project |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches
```
Example response:
@@ -23,6 +23,8 @@ Example response:
{
"name": "master",
"protected": true,
+ "developers_can_push": false,
+ "developers_can_merge": false,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -55,7 +57,7 @@ GET /projects/:id/repository/branches/:branch
| `branch` | string | yes | The name of the branch |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master
```
Example response:
@@ -64,6 +66,8 @@ Example response:
{
"name": "master",
"protected": true,
+ "developers_can_push": false,
+ "developers_can_merge": false,
"commit": {
"author_email": "john@example.com",
"author_name": "John Smith",
@@ -91,13 +95,15 @@ PUT /projects/:id/repository/branches/:branch/protect
```
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
| `branch` | string | yes | The name of the branch |
+| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
+| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
Example response:
@@ -117,7 +123,9 @@ Example response:
]
},
"name": "master",
- "protected": true
+ "protected": true,
+ "developers_can_push": true,
+ "developers_can_merge": true
}
```
@@ -132,7 +140,7 @@ PUT /projects/:id/repository/branches/:branch/unprotect
```
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/branches/master/unprotect
```
| Attribute | Type | Required | Description |
@@ -158,7 +166,9 @@ Example response:
]
},
"name": "master",
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
}
```
@@ -175,7 +185,7 @@ POST /projects/:id/repository/branches
| `ref` | string | yes | The branch name or commit SHA to create branch from |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches?branch_name=newbranch&ref=master"
```
Example response:
@@ -196,7 +206,9 @@ Example response:
]
},
"name": "newbranch",
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
}
```
@@ -218,7 +230,7 @@ It returns `200` if it succeeds, `404` if the branch to be deleted does not exis
or `400` for other reasons. In case of an error, an explaining message is provided.
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/branches/newbranch"
```
Example response:
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
index 0881a7d7a90..1b7a1840138 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 -H "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 -H "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 -X POST -H "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 -X DELETE -H "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 b96f1bdac8a..a21751a49ea 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 -H "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 -H "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 -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "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 -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "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 -X DELETE -H "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 2adea11247e..dce666445d0 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -14,7 +14,7 @@ GET /projects/:id/builds
| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
```
Example of response
@@ -123,7 +123,7 @@ GET /projects/:id/repository/commits/:sha/builds
| `scope` | string **or** array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
```
Example of response
@@ -209,7 +209,7 @@ GET /projects/:id/builds/:build_id
| `build_id` | integer | yes | The ID of a build |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
```
Example of response
@@ -271,7 +271,7 @@ GET /projects/:id/builds/:build_id/artifacts
| `build_id` | integer | yes | The ID of a build |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/artifacts"
```
Response:
@@ -283,6 +283,40 @@ Response:
[ce-2893]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2893
+## Download the artifacts file
+
+> [Introduced][ce-5347] in GitLab 8.10.
+
+Download the artifacts file from the given reference name and job provided the
+build finished successfully.
+
+```
+GET /projects/:id/builds/artifacts/:ref_name/download?job=name
+```
+
+Parameters
+
+| Attribute | Type | Required | Description |
+|-------------|---------|----------|-------------------------- |
+| `id` | integer | yes | The ID of a project |
+| `ref_name` | string | yes | The ref from a repository |
+| `job` | string | yes | The name of the job |
+
+Example request:
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/artifacts/master/download?job=test"
+```
+
+Example response:
+
+| Status | Description |
+|-----------|---------------------------------|
+| 200 | Serves the artifacts file |
+| 404 | Build not found or no artifacts |
+
+[ce-5347]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5347
+
## Get a trace file
Get a trace of a specific build of a project
@@ -297,7 +331,7 @@ GET /projects/:id/builds/:build_id/trace
| build_id | integer | yes | The ID of a build |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8/trace"
```
Response:
@@ -321,7 +355,7 @@ POST /projects/:id/builds/:build_id/cancel
| `build_id` | integer | yes | The ID of a build |
```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
```
Example of response
@@ -367,7 +401,7 @@ POST /projects/:id/builds/:build_id/retry
| `build_id` | integer | yes | The ID of a build |
```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
```
Example of response
@@ -409,7 +443,7 @@ POST /projects/:id/builds/:build_id/erase
Parameters
-| Attribute | Type | required | Description |
+| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `build_id` | integer | yes | The ID of a build |
@@ -417,7 +451,7 @@ Parameters
Example of request
```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/erase"
```
Example of response
@@ -459,7 +493,7 @@ POST /projects/:id/builds/:build_id/artifacts/keep
Parameters
-| Attribute | Type | required | Description |
+| Attribute | Type | Required | Description |
|-------------|---------|----------|---------------------|
| `id` | integer | yes | The ID of a project |
| `build_id` | integer | yes | The ID of a build |
@@ -467,7 +501,7 @@ Parameters
Example request:
```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/artifacts/keep"
```
Example response:
@@ -498,3 +532,49 @@ Example response:
"user": null
}
```
+
+## Play a build
+
+Triggers a manual action to start a build.
+
+```
+POST /projects/:id/builds/:build_id/play
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `build_id` | integer | yes | The ID of a build |
+
+```
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/play"
+```
+
+Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "artifacts_file": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "started",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/api/ci/builds.md b/doc/api/ci/builds.md
index d779463fd8c..2a71b087f19 100644
--- a/doc/api/ci/builds.md
+++ b/doc/api/ci/builds.md
@@ -35,7 +35,7 @@ POST /ci/api/v1/builds/register
```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+curl --request POST "https://gitlab.example.com/ci/api/v1/builds/register" --form "token=t0k3n"
```
### Update details of an existing build
@@ -52,7 +52,7 @@ PUT /ci/api/v1/builds/:id
| `trace` | string | no | The trace of a build |
```
-curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+curl --request PUT "https://gitlab.example.com/ci/api/v1/builds/1234" --form "token=t0k3n" --form "state=running" --form "trace=Running git clone...\n"
```
### Incremental build trace update
@@ -87,7 +87,7 @@ Headers:
| `Content-Range` | string | yes | Bytes range of trace that is sent |
```
-curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+curl --request PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" --header "BUILD-TOKEN=build_t0k3n" --header "Content-Range=0-21" --data "Running git clone...\n"
```
@@ -104,7 +104,7 @@ POST /ci/api/v1/builds/:id/artifacts
| `file` | mixed | yes | Artifacts file |
```
-curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+curl --request POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n" --form "file=@/path/to/file"
```
### Download the artifacts file from build
@@ -119,7 +119,7 @@ GET /ci/api/v1/builds/:id/artifacts
| `token` | string | yes | The build authorization token |
```
-curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
```
### Remove the artifacts file from build
@@ -134,5 +134,5 @@ DELETE /ci/api/v1/builds/:id/artifacts
| `token` | string | yes | The build authorization token |
```
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+curl --request DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" --form "token=build_t0k3n"
```
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index 96b3c42f773..ecec53fde03 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -35,7 +35,7 @@ POST /ci/api/v1/runners/register
Example request:
```sh
-curl -X POST "https://gitlab.example.com/ci/api/v1/runners/register" -F "token=t0k3n"
+curl --request POST "https://gitlab.example.com/ci/api/v1/runners/register" --form "token=t0k3n"
```
## Delete a Runner
@@ -53,5 +53,5 @@ DELETE /ci/api/v1/runners/delete
Example request:
```sh
-curl -X DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" -F "token=t0k3n"
+curl --request DELETE "https://gitlab.example.com/ci/api/v1/runners/delete" --form "token=t0k3n"
```
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 57c2e1d9b87..5c98c5d7565 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -16,7 +16,7 @@ GET /projects/:id/repository/commits
| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits"
```
Example response:
@@ -62,7 +62,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master
```
Example response:
@@ -81,6 +81,11 @@ Example response:
"parent_ids": [
"ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
],
+ "stats": {
+ "additions": 15,
+ "deletions": 10,
+ "total": 25
+ },
"status": "running"
}
```
@@ -101,7 +106,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/diff"
```
Example response:
@@ -137,7 +142,7 @@ Parameters:
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/repository/commits/master/comments"
```
Example response:
@@ -190,7 +195,7 @@ POST /projects/:id/repository/commits/:sha/comments
| `line_type` | string | no | The line type. Takes `new` or `old` as arguments |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "note=Nice picture man\!" -F "path=dudeism.md" -F "line=11" -F "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "note=Nice picture man\!" --form "path=dudeism.md" --form "line=11" --form "line_type=new" https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/comments
```
Example response:
@@ -235,7 +240,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| `all` | boolean | no | Return all statuses, not only the latest ones
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/repository/commits/18f3e63d05582537db6d183d9d557be09e1f90c8/statuses
```
Example response:
@@ -310,7 +315,7 @@ POST /projects/:id/statuses/:sha
| `description` | string | no | The short description of the status
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/17/statuses/18f3e63d05582537db6d183d9d557be09e1f90c8?state=success"
```
Example response:
diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md
index 3ad836f51b5..73cb4b7ea8c 100644
--- a/doc/api/deploy_key_multiple_projects.md
+++ b/doc/api/deploy_key_multiple_projects.md
@@ -7,23 +7,23 @@ First, find the ID of the projects you're interested in, by either listing all
projects:
```
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/projects
```
Or finding the ID of a group and then listing all projects in that group:
```
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups
# For group 1234:
-curl -H 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
+curl --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' https://gitlab.example.com/api/v3/groups/1234
```
With those IDs, add the same deploy key to all:
```
for project_id in 321 456 987; do
- curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" \
- --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/keys
+ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" \
+ --data '{"title": "my key", "key": "ssh-rsa AAAA..."}' https://gitlab.example.com/api/v3/projects/${project_id}/deploy_keys
done
```
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index 9da1fe22e61..ca44afbf355 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -1,11 +1,42 @@
# Deploy Keys
-## List deploy keys
+## List all deploy keys
+
+Get a list of all deploy keys across all projects.
+
+```
+GET /deploy_keys
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/deploy_keys"
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "title": "Public key",
+ "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "created_at": "2013-10-02T10:12:29Z"
+ },
+ {
+ "id": 3,
+ "title": "Another Public key",
+ "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "created_at": "2013-10-02T11:12:29Z"
+ }
+]
+```
+
+## List project deploy keys
Get a list of a project's deploy keys.
```
-GET /projects/:id/keys
+GET /projects/:id/deploy_keys
```
| Attribute | Type | Required | Description |
@@ -13,7 +44,7 @@ GET /projects/:id/keys
| `id` | integer | yes | The ID of the project |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys"
```
Example response:
@@ -40,7 +71,7 @@ Example response:
Get a single key.
```
-GET /projects/:id/keys/:key_id
+GET /projects/:id/deploy_keys/:key_id
```
Parameters:
@@ -51,7 +82,7 @@ Parameters:
| `key_id` | integer | yes | The ID of the deploy key |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/11"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/11"
```
Example response:
@@ -73,7 +104,7 @@ If the deploy key already exists in another project, it will be joined to curren
project only if original one was is accessible by the same user.
```
-POST /projects/:id/keys
+POST /projects/:id/deploy_keys
```
| Attribute | Type | Required | Description |
@@ -83,7 +114,7 @@ POST /projects/:id/keys
| `key` | string | yes | New deploy key |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/keys/"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"title": "My deploy key", "key": "ssh-rsa AAAA..."}' "https://gitlab.example.com/api/v3/projects/5/deploy_keys/"
```
Example response:
@@ -102,7 +133,7 @@ Example response:
Delete a deploy key from a project
```
-DELETE /projects/:id/keys/:key_id
+DELETE /projects/:id/deploy_keys/:key_id
```
| Attribute | Type | Required | Description |
@@ -111,7 +142,7 @@ DELETE /projects/:id/keys/:key_id
| `key_id` | integer | yes | The ID of the deploy key |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/keys/13"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/deploy_keys/13"
```
Example response:
@@ -128,3 +159,51 @@ Example response:
"id" : 13
}
```
+
+## Enable a deploy key
+
+Enables a deploy key for a project so this can be used. Returns the enabled key, with a status code 201 when successful.
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/enable
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `key_id` | integer | yes | The ID of the deploy key |
+
+Example response:
+
+```json
+{
+ "key" : "ssh-rsa AAAA...",
+ "id" : 12,
+ "title" : "My deploy key",
+ "created_at" : "2015-08-29T12:44:31.550Z"
+}
+```
+
+## Disable a deploy key
+
+Disable a deploy key for a project. Returns the disabled key.
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/deploy_keys/13/disable
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the project |
+| `key_id` | integer | yes | The ID of the deploy key |
+
+Example response:
+
+```json
+{
+ "key" : "ssh-rsa AAAA...",
+ "id" : 12,
+ "title" : "My deploy key",
+ "created_at" : "2015-08-29T12:44:31.550Z"
+}
+```
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
new file mode 100644
index 00000000000..417962de82d
--- /dev/null
+++ b/doc/api/deployments.md
@@ -0,0 +1,218 @@
+# Deployments API
+
+## List project deployments
+
+Get a list of deployments in a project.
+
+```
+GET /projects/:id/deployments
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments"
+```
+
+Example of response
+
+```json
+[
+ {
+ "created_at": "2016-08-11T07:36:40.222Z",
+ "deployable": {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2016-08-11T09:36:01.000+02:00",
+ "id": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+ "message": "Merge branch 'new-title' into 'master'\r\n\r\nUpdate README\r\n\r\n\r\n\r\nSee merge request !1",
+ "short_id": "99d03678",
+ "title": "Merge branch 'new-title' into 'master'\r"
+ },
+ "coverage": null,
+ "created_at": "2016-08-11T07:36:27.357Z",
+ "finished_at": "2016-08-11T07:36:39.851Z",
+ "id": 657,
+ "name": "deploy",
+ "ref": "master",
+ "runner": null,
+ "stage": "deploy",
+ "started_at": null,
+ "status": "success",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "location": null,
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root",
+ "website_url": ""
+ }
+ },
+ "environment": {
+ "external_url": "https://about.gitlab.com",
+ "id": 9,
+ "name": "production"
+ },
+ "id": 41,
+ "iid": 1,
+ "ref": "master",
+ "sha": "99d03678b90d914dbb1b109132516d71a4a03ea8",
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ },
+ {
+ "created_at": "2016-08-11T11:32:35.444Z",
+ "deployable": {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2016-08-11T13:28:26.000+02:00",
+ "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2",
+ "short_id": "a91957a8",
+ "title": "Merge branch 'rename-readme' into 'master'\r"
+ },
+ "coverage": null,
+ "created_at": "2016-08-11T11:32:24.456Z",
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "id": 664,
+ "name": "deploy",
+ "ref": "master",
+ "runner": null,
+ "stage": "deploy",
+ "started_at": null,
+ "status": "success",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "location": null,
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root",
+ "website_url": ""
+ }
+ },
+ "environment": {
+ "external_url": "https://about.gitlab.com",
+ "id": 9,
+ "name": "production"
+ },
+ "id": 42,
+ "iid": 2,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "id": 1,
+ "name": "Administrator",
+ "state": "active",
+ "username": "root",
+ "web_url": "http://localhost:3000/u/root"
+ }
+ }
+]
+```
+
+## Get a specific deployment
+
+```
+GET /projects/:id/deployments/:deployment_id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `deployment_id` | integer | yes | The ID of the deployment |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/deployments/1"
+```
+
+Example of response
+
+```json
+{
+ "id": 42,
+ "iid": 2,
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "created_at": "2016-08-11T11:32:35.444Z",
+ "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/u/root"
+ },
+ "environment": {
+ "id": 9,
+ "name": "production",
+ "external_url": "https://about.gitlab.com"
+ },
+ "deployable": {
+ "id": 664,
+ "status": "success",
+ "stage": "deploy",
+ "name": "deploy",
+ "ref": "master",
+ "tag": false,
+ "coverage": null,
+ "created_at": "2016-08-11T11:32:24.456Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "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/u/root",
+ "created_at": "2016-08-11T07:09:20.351Z",
+ "is_admin": true,
+ "bio": null,
+ "location": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": ""
+ },
+ "commit": {
+ "id": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "short_id": "a91957a8",
+ "title": "Merge branch 'rename-readme' into 'master'\r",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-08-11T13:28:26.000+02:00",
+ "message": "Merge branch 'rename-readme' into 'master'\r\n\r\nRename README\r\n\r\n\r\n\r\nSee merge request !2"
+ },
+ "runner": null
+ }
+}
+```
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
new file mode 100644
index 00000000000..87a5fa67124
--- /dev/null
+++ b/doc/api/enviroments.md
@@ -0,0 +1,117 @@
+# Environments
+
+## List environments
+
+Get all environments for a given project.
+
+```
+GET /projects/:id/environments
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/environments
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Env1",
+ "external_url": "https://env1.example.gitlab.com"
+ }
+]
+```
+
+## Create a new environment
+
+Creates a new environment with the given name and external_url.
+
+It returns 201 if the environment was successfully created, 400 for wrong parameters.
+
+```
+POST /projects/:id/environment
+```
+
+| Attribute | Type | Required | Description |
+| ------------- | ------- | -------- | ---------------------------- |
+| `id` | integer | yes | The ID of the project |
+| `name` | string | yes | The name of the environment |
+| `external_url` | string | no | Place to link to for this environment |
+
+```bash
+curl --data "name=deploy&external_url=https://deploy.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environments"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "deploy",
+ "external_url": "https://deploy.example.gitlab.com"
+}
+```
+
+## Edit an existing environment
+
+Updates an existing environment's name and/or external_url.
+
+It returns 200 if the environment was successfully updated. In case of an error, a status code 400 is returned.
+
+```
+PUT /projects/:id/environments/:environments_id
+```
+
+| Attribute | Type | Required | Description |
+| --------------- | ------- | --------------------------------- | ------------------------------- |
+| `id` | integer | yes | The ID of the project |
+| `environment_id` | integer | yes | The ID of the environment | The ID of the environment |
+| `name` | string | no | The new name of the environment |
+| `external_url` | string | no | The new external_url |
+
+```bash
+curl --request PUT --data "name=staging&external_url=https://staging.example.gitlab.com" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "staging",
+ "external_url": "https://staging.example.gitlab.com"
+}
+```
+
+## Delete an environment
+
+It returns 200 if the environment was successfully deleted, and 404 if the environment does not exist.
+
+```
+DELETE /projects/:id/environments/:environment_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | integer | yes | The ID of the project |
+| `environment_id` | integer | yes | The ID of the environment |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/environment/1"
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "name": "deploy",
+ "external_url": "https://deploy.example.gitlab.com"
+}
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 87480bebfc4..a898387eaa2 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,514 +1,434 @@
-# Groups
-
-## List groups
-
-Get a list of groups. (As user: my groups, as admin: all groups)
-
-```
-GET /groups
-```
-
-```json
-[
- {
- "id": 1,
- "name": "Foobar Group",
- "path": "foo-bar",
- "description": "An interesting group"
- }
-]
-```
-
-You can search for groups by name or path, see below.
-
-
-## List a group's projects
-
-Get a list of projects in this group.
-
-```
-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
-
-```json
-[
- {
- "id": 9,
- "description": "foo",
- "default_branch": "master",
- "tag_list": [],
- "public": false,
- "archived": false,
- "visibility_level": 10,
- "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
- "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
- "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
- "name": "Html5 Boilerplate",
- "name_with_namespace": "Experimental / Html5 Boilerplate",
- "path": "html5-boilerplate",
- "path_with_namespace": "h5bp/html5-boilerplate",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "builds_enabled": true,
- "snippets_enabled": true,
- "created_at": "2016-04-05T21:40:50.169Z",
- "last_activity_at": "2016-04-06T16:52:08.432Z",
- "shared_runners_enabled": true,
- "creator_id": 1,
- "namespace": {
- "id": 5,
- "name": "Experimental",
- "path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
- },
- "avatar_url": null,
- "star_count": 1,
- "forks_count": 0,
- "open_issues_count": 3,
- "public_builds": true,
- "shared_with_groups": []
- }
-]
-```
-
-## Details of a group
-
-Get all details of a group.
-
-```
-GET /groups/:id
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID or path of a group |
-
-```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
-```
-
-Example response:
-
-```json
-{
- "id": 4,
- "name": "Twitter",
- "path": "twitter",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "visibility_level": 20,
- "avatar_url": null,
- "web_url": "https://gitlab.example.com/groups/twitter",
- "projects": [
- {
- "id": 7,
- "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
- "default_branch": "master",
- "tag_list": [],
- "public": true,
- "archived": false,
- "visibility_level": 20,
- "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
- "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
- "web_url": "https://gitlab.example.com/twitter/typeahead-js",
- "name": "Typeahead.Js",
- "name_with_namespace": "Twitter / Typeahead.Js",
- "path": "typeahead-js",
- "path_with_namespace": "twitter/typeahead-js",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "builds_enabled": true,
- "snippets_enabled": false,
- "container_registry_enabled": true,
- "created_at": "2016-06-17T07:47:25.578Z",
- "last_activity_at": "2016-06-17T07:47:25.881Z",
- "shared_runners_enabled": true,
- "creator_id": 1,
- "namespace": {
- "id": 4,
- "name": "Twitter",
- "path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
- },
- "avatar_url": null,
- "star_count": 0,
- "forks_count": 0,
- "open_issues_count": 3,
- "public_builds": true,
- "shared_with_groups": []
- },
- {
- "id": 6,
- "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
- "default_branch": "master",
- "tag_list": [],
- "public": false,
- "archived": false,
- "visibility_level": 10,
- "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
- "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
- "web_url": "https://gitlab.example.com/twitter/flight",
- "name": "Flight",
- "name_with_namespace": "Twitter / Flight",
- "path": "flight",
- "path_with_namespace": "twitter/flight",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "builds_enabled": true,
- "snippets_enabled": false,
- "container_registry_enabled": true,
- "created_at": "2016-06-17T07:47:24.661Z",
- "last_activity_at": "2016-06-17T07:47:24.838Z",
- "shared_runners_enabled": true,
- "creator_id": 1,
- "namespace": {
- "id": 4,
- "name": "Twitter",
- "path": "twitter",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:24.216Z",
- "updated_at": "2016-06-17T07:47:24.216Z",
- "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
- },
- "avatar_url": null,
- "star_count": 0,
- "forks_count": 0,
- "open_issues_count": 8,
- "public_builds": true,
- "shared_with_groups": []
- }
- ],
- "shared_projects": [
- {
- "id": 8,
- "description": "Velit eveniet provident fugiat saepe eligendi autem.",
- "default_branch": "master",
- "tag_list": [],
- "public": false,
- "archived": false,
- "visibility_level": 0,
- "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
- "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
- "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
- "name": "Html5 Boilerplate",
- "name_with_namespace": "H5bp / Html5 Boilerplate",
- "path": "html5-boilerplate",
- "path_with_namespace": "h5bp/html5-boilerplate",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "builds_enabled": true,
- "snippets_enabled": false,
- "container_registry_enabled": true,
- "created_at": "2016-06-17T07:47:27.089Z",
- "last_activity_at": "2016-06-17T07:47:27.310Z",
- "shared_runners_enabled": true,
- "creator_id": 1,
- "namespace": {
- "id": 5,
- "name": "H5bp",
- "path": "h5bp",
- "owner_id": null,
- "created_at": "2016-06-17T07:47:26.621Z",
- "updated_at": "2016-06-17T07:47:26.621Z",
- "description": "Id consequatur rem vel qui doloremque saepe.",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 20
- },
- "avatar_url": null,
- "star_count": 0,
- "forks_count": 0,
- "open_issues_count": 4,
- "public_builds": true,
- "shared_with_groups": [
- {
- "group_id": 4,
- "group_name": "Twitter",
- "group_access_level": 30
- },
- {
- "group_id": 3,
- "group_name": "Gitlab Org",
- "group_access_level": 10
- }
- ]
- }
- ]
-}
-```
-
-## New group
-
-Creates a new project group. Available only for users who can create groups.
-
-```
-POST /groups
-```
-
-Parameters:
-
-- `name` (required) - The name of the group
-- `path` (required) - The path of the group
-- `description` (optional) - The group's description
-- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
-
-## Transfer project to group
-
-Transfer a project to the Group namespace. Available only for admin
-
-```
-POST /groups/:id/projects/:project_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a group
-- `project_id` (required) - The ID of a project
-
-## Update group
-
-Updates the project group. Only available to group owners and administrators.
-
-```
-PUT /groups/:id
-```
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the group |
-| `name` | string | no | The name of the group |
-| `path` | string | no | The path of the group |
-| `description` | string | no | The description of the group |
-| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
-
-```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
-
-```
-
-Example response:
-
-```json
-{
- "id": 5,
- "name": "Experimental",
- "path": "h5bp",
- "description": "foo",
- "visibility_level": 10,
- "avatar_url": null,
- "web_url": "http://gitlab.example.com/groups/h5bp",
- "projects": [
- {
- "id": 9,
- "description": "foo",
- "default_branch": "master",
- "tag_list": [],
- "public": false,
- "archived": false,
- "visibility_level": 10,
- "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
- "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
- "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
- "name": "Html5 Boilerplate",
- "name_with_namespace": "Experimental / Html5 Boilerplate",
- "path": "html5-boilerplate",
- "path_with_namespace": "h5bp/html5-boilerplate",
- "issues_enabled": true,
- "merge_requests_enabled": true,
- "wiki_enabled": true,
- "builds_enabled": true,
- "snippets_enabled": true,
- "created_at": "2016-04-05T21:40:50.169Z",
- "last_activity_at": "2016-04-06T16:52:08.432Z",
- "shared_runners_enabled": true,
- "creator_id": 1,
- "namespace": {
- "id": 5,
- "name": "Experimental",
- "path": "h5bp",
- "owner_id": null,
- "created_at": "2016-04-05T21:40:49.152Z",
- "updated_at": "2016-04-07T08:07:48.466Z",
- "description": "foo",
- "avatar": {
- "url": null
- },
- "share_with_group_lock": false,
- "visibility_level": 10
- },
- "avatar_url": null,
- "star_count": 1,
- "forks_count": 0,
- "open_issues_count": 3,
- "public_builds": true,
- "shared_with_groups": []
- }
- ]
-}
-```
-
-## Remove group
-
-Removes group with all projects inside.
-
-```
-DELETE /groups/:id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a user group
-
-## Search for group
-
-Get all groups that match your string in their name or path.
-
-```
-GET /groups?search=foobar
-```
-
-```json
-[
- {
- "id": 1,
- "name": "Foobar Group",
- "path": "foo-bar",
- "description": "An interesting group"
- }
-]
-```
-
-## Group members
-
-**Group access levels**
-
-The group access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
-
-```
-GUEST = 10
-REPORTER = 20
-DEVELOPER = 30
-MASTER = 40
-OWNER = 50
-```
-
-### List group members
-
-Get a list of group members viewable by the authenticated user.
-
-```
-GET /groups/:id/members
-```
-
-```json
-[
- {
- "id": 1,
- "username": "raymond_smith",
- "name": "Raymond Smith",
- "state": "active",
- "created_at": "2012-10-22T14:13:35Z",
- "access_level": 30
- },
- {
- "id": 2,
- "username": "john_doe",
- "name": "John Doe",
- "state": "active",
- "created_at": "2012-10-22T14:13:35Z",
- "access_level": 30
- }
-]
-```
-
-### Add group member
-
-Adds a user to the list of group members.
-
-```
-POST /groups/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a group
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit group team member
-
-Updates a group team member to a specified access level.
-
-```
-PUT /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID of a group
-- `user_id` (required) - The ID of a group member
-- `access_level` (required) - Project access level
-
-### Remove user team member
-
-Removes user from user team.
-
-```
-DELETE /groups/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or path of a user group
-- `user_id` (required) - The ID of a group member
-
-## Namespaces in groups
-
-By default, groups only get 20 namespaces at a time because the API results are paginated.
-
-To get more (up to 100), pass the following as an argument to the API call:
-```
-/groups?per_page=100
-```
-
-And to switch pages add:
-```
-/groups?per_page=100&page=2
-```
+# Groups
+
+## List groups
+
+Get a list of groups. (As user: my groups, as admin: all groups)
+
+```
+GET /groups
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group"
+ }
+]
+```
+
+You can search for groups by name or path, see below.
+
+
+## List a group's projects
+
+Get a list of projects in this group.
+
+```
+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
+
+```json
+[
+ {
+ "id": 9,
+ "description": "foo",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+ "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "Experimental / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2016-04-05T21:40:50.169Z",
+ "last_activity_at": "2016-04-06T16:52:08.432Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-04-05T21:40:49.152Z",
+ "updated_at": "2016-04-07T08:07:48.466Z",
+ "description": "foo",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 10
+ },
+ "avatar_url": null,
+ "star_count": 1,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+]
+```
+
+## Details of a group
+
+Get all details of a group.
+
+```
+GET /groups/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or path of a group |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4
+```
+
+Example response:
+
+```json
+{
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "visibility_level": 20,
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/groups/twitter",
+ "projects": [
+ {
+ "id": 7,
+ "description": "Voluptas veniam qui et beatae voluptas doloremque explicabo facilis.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": true,
+ "archived": false,
+ "visibility_level": 20,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/typeahead-js.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/typeahead-js.git",
+ "web_url": "https://gitlab.example.com/twitter/typeahead-js",
+ "name": "Typeahead.Js",
+ "name_with_namespace": "Twitter / Typeahead.Js",
+ "path": "typeahead-js",
+ "path_with_namespace": "twitter/typeahead-js",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:25.578Z",
+ "last_activity_at": "2016-06-17T07:47:25.881Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": "Aspernatur omnis repudiandae qui voluptatibus eaque.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com:twitter/flight.git",
+ "http_url_to_repo": "https://gitlab.example.com/twitter/flight.git",
+ "web_url": "https://gitlab.example.com/twitter/flight",
+ "name": "Flight",
+ "name_with_namespace": "Twitter / Flight",
+ "path": "flight",
+ "path_with_namespace": "twitter/flight",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:24.661Z",
+ "last_activity_at": "2016-06-17T07:47:24.838Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 4,
+ "name": "Twitter",
+ "path": "twitter",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:24.216Z",
+ "updated_at": "2016-06-17T07:47:24.216Z",
+ "description": "Aliquid qui quis dignissimos distinctio ut commodi voluptas est.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 8,
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+ ],
+ "shared_projects": [
+ {
+ "id": 8,
+ "description": "Velit eveniet provident fugiat saepe eligendi autem.",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@gitlab.example.com:h5bp/html5-boilerplate.git",
+ "http_url_to_repo": "https://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "https://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "H5bp / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": true,
+ "created_at": "2016-06-17T07:47:27.089Z",
+ "last_activity_at": "2016-06-17T07:47:27.310Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "H5bp",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-06-17T07:47:26.621Z",
+ "updated_at": "2016-06-17T07:47:26.621Z",
+ "description": "Id consequatur rem vel qui doloremque saepe.",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 20
+ },
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "open_issues_count": 4,
+ "public_builds": true,
+ "shared_with_groups": [
+ {
+ "group_id": 4,
+ "group_name": "Twitter",
+ "group_access_level": 30
+ },
+ {
+ "group_id": 3,
+ "group_name": "Gitlab Org",
+ "group_access_level": 10
+ }
+ ]
+ }
+ ]
+}
+```
+
+## New group
+
+Creates a new project group. Available only for users who can create groups.
+
+```
+POST /groups
+```
+
+Parameters:
+
+- `name` (required) - The name of the group
+- `path` (required) - The path of the group
+- `description` (optional) - The group's description
+- `visibility_level` (optional) - The group's visibility. 0 for private, 10 for internal, 20 for public.
+
+## Transfer project to group
+
+Transfer a project to the Group namespace. Available only for admin
+
+```
+POST /groups/:id/projects/:project_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a group
+- `project_id` (required) - The ID of a project
+
+## Update group
+
+Updates the project group. Only available to group owners and administrators.
+
+```
+PUT /groups/:id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of the group |
+| `name` | string | no | The name of the group |
+| `path` | string | no | The path of the group |
+| `description` | string | no | The description of the group |
+| `visibility_level` | integer | no | The visibility level of the group. 0 for private, 10 for internal, 20 for public. |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/groups/5?name=Experimental"
+
+```
+
+Example response:
+
+```json
+{
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "description": "foo",
+ "visibility_level": 10,
+ "avatar_url": null,
+ "web_url": "http://gitlab.example.com/groups/h5bp",
+ "projects": [
+ {
+ "id": 9,
+ "description": "foo",
+ "default_branch": "master",
+ "tag_list": [],
+ "public": false,
+ "archived": false,
+ "visibility_level": 10,
+ "ssh_url_to_repo": "git@gitlab.example.com/html5-boilerplate.git",
+ "http_url_to_repo": "http://gitlab.example.com/h5bp/html5-boilerplate.git",
+ "web_url": "http://gitlab.example.com/h5bp/html5-boilerplate",
+ "name": "Html5 Boilerplate",
+ "name_with_namespace": "Experimental / Html5 Boilerplate",
+ "path": "html5-boilerplate",
+ "path_with_namespace": "h5bp/html5-boilerplate",
+ "issues_enabled": true,
+ "merge_requests_enabled": true,
+ "wiki_enabled": true,
+ "builds_enabled": true,
+ "snippets_enabled": true,
+ "created_at": "2016-04-05T21:40:50.169Z",
+ "last_activity_at": "2016-04-06T16:52:08.432Z",
+ "shared_runners_enabled": true,
+ "creator_id": 1,
+ "namespace": {
+ "id": 5,
+ "name": "Experimental",
+ "path": "h5bp",
+ "owner_id": null,
+ "created_at": "2016-04-05T21:40:49.152Z",
+ "updated_at": "2016-04-07T08:07:48.466Z",
+ "description": "foo",
+ "avatar": {
+ "url": null
+ },
+ "share_with_group_lock": false,
+ "visibility_level": 10
+ },
+ "avatar_url": null,
+ "star_count": 1,
+ "forks_count": 0,
+ "open_issues_count": 3,
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+ ]
+}
+```
+
+## Remove group
+
+Removes group with all projects inside.
+
+```
+DELETE /groups/:id
+```
+
+Parameters:
+
+- `id` (required) - The ID or path of a user group
+
+## Search for group
+
+Get all groups that match your string in their name or path.
+
+```
+GET /groups?search=foobar
+```
+
+```json
+[
+ {
+ "id": 1,
+ "name": "Foobar Group",
+ "path": "foo-bar",
+ "description": "An interesting group"
+ }
+]
+```
+
+## Group members
+
+Please consult the [Group Members](members.md) documentation.
+
+## Namespaces in groups
+
+By default, groups only get 20 namespaces at a time because the API results are paginated.
+
+To get more (up to 100), pass the following as an argument to the API call:
+```
+/groups?per_page=100
+```
+
+And to switch pages add:
+```
+/groups?per_page=100&page=2
+```
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 3ced787b23e..b194799ccbf 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -33,7 +33,7 @@ GET /issues?labels=foo,bar&state=opened
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/issues
```
Example response:
@@ -78,7 +78,9 @@ Example response:
"iid" : 6,
"labels" : [],
"subscribed" : false,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/6"
}
]
```
@@ -109,7 +111,7 @@ GET /groups/:id/issues?milestone=1.0.0&state=opened
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/4/issues
```
Example response:
@@ -154,7 +156,9 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed" : false,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/1"
}
]
```
@@ -187,7 +191,7 @@ GET /projects/:id/issues?iid=42
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues
```
Example response:
@@ -232,7 +236,9 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed" : false,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/1"
}
]
```
@@ -251,7 +257,7 @@ GET /projects/:id/issues/:issue_id
| `issue_id`| integer | yes | The ID of a project's issue |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/41
```
Example response:
@@ -295,7 +301,9 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed": false,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/1"
}
```
@@ -319,10 +327,11 @@ POST /projects/:id/issues
| `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `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` |
+| `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` |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
```
Example response:
@@ -351,7 +360,9 @@ Example response:
"updated_at" : "2016-01-07T12:44:33.959Z",
"milestone" : null,
"subscribed" : true,
- "user_notes_count": 0
+ "user_notes_count": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/14"
}
```
@@ -378,10 +389,11 @@ PUT /projects/:id/issues/:issue_id
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
-| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
+| `updated_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` |
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
```
Example response:
@@ -410,7 +422,9 @@ Example response:
"assignee" : null,
"milestone" : null,
"subscribed" : true,
- "user_notes_count": 0
+ "user_notes_count": 0,
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/15"
}
```
@@ -430,7 +444,7 @@ DELETE /projects/:id/issues/:issue_id
| `issue_id` | integer | yes | The ID of a project's issue |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85
```
## Move an issue
@@ -455,7 +469,7 @@ POST /projects/:id/issues/:issue_id/move
| `to_project_id` | integer | yes | The ID of the new project |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85/move
```
Example response:
@@ -487,7 +501,9 @@ Example response:
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin"
- }
+ },
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11"
}
```
@@ -509,7 +525,7 @@ POST /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
```
Example response:
@@ -541,7 +557,9 @@ Example response:
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin"
- }
+ },
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11"
}
```
@@ -563,7 +581,7 @@ DELETE /projects/:id/issues/:issue_id/subscription
| `issue_id` | integer | yes | The ID of a project's issue |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
```
Example response:
@@ -596,7 +614,9 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/orville"
},
- "subscribed": false
+ "subscribed": false,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/12"
}
```
@@ -617,7 +637,7 @@ POST /projects/:id/issues/:issue_id/todo
| `issue_id` | integer | yes | The ID of a project's issue |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
```
Example response:
@@ -682,7 +702,9 @@ Example response:
"subscribed": true,
"user_notes_count": 7,
"upvotes": 0,
- "downvotes": 0
+ "downvotes": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/110"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/labels.md b/doc/api/labels.md
index a181c0f57a2..3653ccf304a 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -13,7 +13,7 @@ GET /projects/:id/labels
| `id` | integer | yes | The ID of the project |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/labels
```
Example response:
@@ -82,7 +82,7 @@ POST /projects/:id/labels
| `description` | string | no | The description of the label |
```bash
-curl --data "name=feature&color=#5843AD" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --data "name=feature&color=#5843AD" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
```
Example response:
@@ -113,7 +113,7 @@ DELETE /projects/:id/labels
| `name` | string | yes | The name of the label |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels?name=bug"
```
Example response:
@@ -153,7 +153,7 @@ PUT /projects/:id/labels
| `description` | string | no | The new description of the label |
```bash
-curl -X PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
+curl --request PUT --data "name=documentation&new_name=docs&color=#8E44AD&description=Documentation" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/labels"
```
Example response:
@@ -184,7 +184,7 @@ POST /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
```
Example response:
@@ -219,7 +219,7 @@ DELETE /projects/:id/labels/:label_id/subscription
| `label_id` | integer or string | yes | The ID or title of a project's label |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/labels/1/subscription
```
Example response:
diff --git a/doc/api/licenses.md b/doc/api/licenses.md
index 855b0eab56f..ed26d1fb7fb 100644
--- a/doc/api/licenses.md
+++ b/doc/api/licenses.md
@@ -116,7 +116,7 @@ If you omit the `fullname` parameter but authenticate your request, the name of
the authenticated user will be used to replace the copyright holder placeholder.
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
```
Example response:
diff --git a/doc/api/members.md b/doc/api/members.md
new file mode 100644
index 00000000000..fd6d728dad2
--- /dev/null
+++ b/doc/api/members.md
@@ -0,0 +1,185 @@
+# Group and project members
+
+**Valid access levels**
+
+The access levels are defined in the `Gitlab::Access` module. Currently, these levels are recognized:
+
+```
+10 => Guest access
+20 => Reporter access
+30 => Developer access
+40 => Master access
+50 => Owner access # Only valid for groups
+```
+
+## List all members of a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `query` | string | no | A query string to search for members |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30
+ },
+ {
+ "id": 2,
+ "username": "john_doe",
+ "name": "John Doe",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30
+ }
+]
+```
+
+## Get a member of a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30,
+ "expires_at": null
+}
+```
+
+## Add a member to a group or project
+
+Adds a member to a group or project.
+
+Returns `201` if the request succeeds.
+
+```
+POST /groups/:id/members
+POST /projects/:id/members
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the new member |
+| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 30
+}
+```
+
+## Edit a member of a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the member |
+| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=40
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "username": "raymond_smith",
+ "name": "Raymond Smith",
+ "state": "active",
+ "created_at": "2012-10-22T14:13:35Z",
+ "access_level": 40
+}
+```
+
+## Remove a member from a group or project
+
+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
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The group/project ID or path |
+| `user_id` | integer | yes | The user ID of the member |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 816f09e1007..f4760ceac7c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -68,7 +68,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : false,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
]
```
@@ -132,7 +135,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -233,6 +239,9 @@ Parameters:
"merge_status": "can_be_merged",
"subscribed" : true,
"user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1",
"changes": [
{
"old_path": "VERSION",
@@ -270,6 +279,7 @@ Parameters:
```json
{
"id": 1,
+ "iid": 1,
"target_branch": "master",
"source_branch": "test1",
"project_id": 3,
@@ -312,7 +322,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
- "user_notes_count": 0
+ "user_notes_count": 0,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -342,6 +355,7 @@ Parameters:
```json
{
"id": 1,
+ "iid": 1,
"target_branch": "master",
"project_id": 3,
"title": "test1",
@@ -383,7 +397,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -406,7 +423,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id
| `merge_request_id` | integer | yes | The ID of a project's merge request |
```bash
-curl -X DELETE -H "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_request/85
```
## Accept MR
@@ -439,6 +456,7 @@ Parameters:
```json
{
"id": 1,
+ "iid": 1,
"target_branch": "master",
"source_branch": "test1",
"project_id": 3,
@@ -481,7 +499,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -505,6 +526,7 @@ Parameters:
```json
{
"id": 1,
+ "iid": 1,
"target_branch": "master",
"source_branch": "test1",
"project_id": 3,
@@ -547,7 +569,10 @@ Parameters:
"merge_when_build_succeeds": true,
"merge_status": "can_be_merged",
"subscribed" : true,
- "user_notes_count": 1
+ "user_notes_count": 1,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -569,7 +594,7 @@ GET /projects/:id/merge_requests/:merge_request_id/closes_issues
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/76/merge_requests/1/closes_issues
```
Example response when the GitLab issue tracker is used:
@@ -647,7 +672,7 @@ POST /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
```
Example response:
@@ -721,7 +746,7 @@ DELETE /projects/:id/merge_requests/:merge_request_id/subscription
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
```
Example response:
@@ -794,7 +819,7 @@ POST /projects/:id/merge_requests/:merge_request_id/todo
| `merge_request_id` | integer | yes | The ID of the merge request |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
```
Example response:
@@ -866,7 +891,10 @@ Example response:
"merge_when_build_succeeds": false,
"merge_status": "unchecked",
"subscribed": true,
- "user_notes_count": 7
+ "user_notes_count": 7,
+ "should_remove_source_branch": true,
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
"body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
@@ -874,3 +902,112 @@ Example response:
"created_at": "2016-07-01T11:14:15.530Z"
}
```
+
+## Get MR diff versions
+
+Get a list of merge request diff versions.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+```
+
+Example response:
+
+```json
+[{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}, {
+ "id": 108,
+ "head_commit_sha": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-25T14:21:33.028Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}]
+```
+
+## Get a single MR diff version
+
+Get a single merge request diff version.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+| `version_id` | integer | yes | The ID of the merge request diff version |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+```
+
+Example response:
+
+```json
+{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1",
+ "commits": [{
+ "id": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "short_id": "33e2ee85",
+ "title": "Change year to 2018",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-26T17:44:29.000+03:00",
+ "message": "Change year to 2018"
+ }, {
+ "id": "aa24655de48b36335556ac8a3cd8bb521f977cbd",
+ "short_id": "aa24655d",
+ "title": "Update LICENSE",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:53.000+03:00",
+ "message": "Update LICENSE"
+ }, {
+ "id": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "short_id": "3eed087b",
+ "title": "Add license",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:20.000+03:00",
+ "message": "Add license"
+ }],
+ "diffs": [{
+ "old_path": "LICENSE",
+ "new_path": "LICENSE",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "diff": "--- /dev/null\n+++ b/LICENSE\n@@ -0,0 +1,21 @@\n+The MIT License (MIT)\n+\n+Copyright (c) 2018 Administrator\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ }]
+}
+```
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index e4202025f80..ae7d22a4be5 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -20,7 +20,7 @@ Parameters:
| `state` | string | optional | Return only `active` or `closed` milestones` |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/milestones
```
Example Response:
diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md
index 42d9ce3d391..88cd407d792 100644
--- a/doc/api/namespaces.md
+++ b/doc/api/namespaces.md
@@ -19,7 +19,7 @@ GET /namespaces
Example request:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces
```
Example response:
@@ -54,7 +54,7 @@ GET /namespaces?search=foobar
Example request:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/namespaces?search=twitter
```
Example response:
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 7aa1c2155bf..85d140d06ac 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -124,7 +124,7 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
```
Example Response:
@@ -248,7 +248,7 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
```
Example Response:
@@ -376,7 +376,7 @@ Parameters:
| `note_id` | integer | yes | The ID of a note |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
```
Example Response:
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 31902e145f6..0b0fc39ec7e 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,41 +1,62 @@
# GitLab as an OAuth2 client
-This document is about using other OAuth authentication service providers to sign into GitLab.
-If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
+This document covers using the OAuth2 protocol to access GitLab.
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password.
+If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
-Before using the OAuth2 you should create an application in user's account. Each application gets a unique App ID and App Secret parameters. You should not share these.
+OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
## Web Application Flow
-This flow is using for authentication from third-party web sites and is probably used the most.
-It basically consists of an exchange of an authorization token for an access token. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1)
+This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf.
+
+>**Note:**
+This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported).
-This flow consists from 3 steps.
+For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1)
+
+In the following sections you will be introduced to the three steps needed for this flow.
### 1. Registering the client
-Create an application in user's account profile.
+First, you should create an application (`/profile/applications`) in your user's account.
+Each application gets a unique App ID and App Secret parameters.
+
+>**Note:**
+**You should not share/leak your App ID or App Secret.**
### 2. Requesting authorization
-To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that by visiting manually the URL:
+To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint:
+
+```
+https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash
+```
+
+This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided.
+
+The redirect will include the GET `code` parameter, for example:
```
-http://localhost:3000/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code
+http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
```
-Where REDIRECT_URI is the URL in your app where users will be sent after authorization.
+You should then use the `code` to request an access token.
+
+>**Important:**
+It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
+validate that value is returned and matches in the redirect request.
+This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
+`state` really should have been a requirement in the standard!
### 3. Requesting the access token
-To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. In this case, I used rest-client:
+Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`:
```
-parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=AUTHORIZATION_CODE&redirect_uri=REDIRECT_URI'
+parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
RestClient.post 'http://localhost:3000/oauth/token', parameters
# The response will be
@@ -46,6 +67,8 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
"refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
}
```
+>**Note:**
+The `redirect_uri` must match the `redirect_uri` used in the original authorization request.
You can now make requests to the API with the access token returned.
@@ -60,14 +83,14 @@ GET https://localhost:3000/api/v3/user?access_token=OAUTH-TOKEN
Or you can put the token to the Authorization header:
```
-curl -H "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
+curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/user
```
## Resource Owner Password Credentials
## Deprecation Notice
-1. Starting in GitLab 9.0, the Resource Owner Password Credentials will be *disabled* for users with two-factor authentication turned on.
+1. Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication turned on.
2. These users can access the API using [personal access tokens] instead.
---
@@ -77,6 +100,9 @@ The credentials should only be used when there is a high degree of trust between
client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
available (such as an authorization code).
+>**Important:**
+Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice.
+
Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used
for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the
resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token.
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
new file mode 100644
index 00000000000..847408a7f61
--- /dev/null
+++ b/doc/api/pipelines.md
@@ -0,0 +1,207 @@
+# Pipelines API
+
+## List project pipelines
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines"
+```
+
+Example of response
+
+```json
+[
+ {
+ "id": 47,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "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/u/root"
+ },
+ "created_at": "2016-08-16T10:23:19.007Z",
+ "updated_at": "2016-08-16T10:23:19.216Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null
+ },
+ {
+ "id": 48,
+ "status": "pending",
+ "ref": "new-pipeline",
+ "sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
+ "before_sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
+ "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/u/root"
+ },
+ "created_at": "2016-08-16T10:23:21.184Z",
+ "updated_at": "2016-08-16T10:23:21.314Z",
+ "started_at": null,
+ "finished_at": null,
+ "committed_at": null,
+ "duration": null
+ }
+]
+```
+
+## Get a single pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+GET /projects/:id/pipelines/:pipeline_id
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipeline/46"
+```
+
+Example of response
+
+```json
+{
+ "id": 46,
+ "status": "success",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "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/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+## Retry failed builds in a pipeline
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/retry
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/retry"
+```
+
+Response:
+
+```json
+{
+ "id": 46,
+ "status": "pending",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "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/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+## Cancel a pipelines builds
+
+> [Introduced][ce-5837] in GitLab 8.11
+
+```
+POST /projects/:id/pipelines/:pipeline_id/cancel
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/pipelines/46/cancel"
+```
+
+Response:
+
+```json
+{
+ "id": 46,
+ "status": "canceled",
+ "ref": "master",
+ "sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "before_sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
+ "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/u/root"
+ },
+ "created_at": "2016-08-11T11:28:34.085Z",
+ "updated_at": "2016-08-11T11:32:35.169Z",
+ "started_at": null,
+ "finished_at": "2016-08-11T11:32:35.145Z",
+ "committed_at": null,
+ "duration": null
+}
+```
+
+[ce-5837]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5837
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index a7acf37b5bc..c6685f54a9d 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -53,7 +53,8 @@ Parameters:
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
- "created_at": "2012-06-28T10:52:04Z"
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/example/example/snippets/1"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index dceee7b4ea7..0e4806e31c5 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -84,7 +84,8 @@ Parameters:
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
},
{
"id": 6,
@@ -144,7 +145,8 @@ Parameters:
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
]
```
@@ -280,7 +282,8 @@ Parameters:
"group_name": "Gitlab Org",
"group_access_level": 10
}
- ]
+ ],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -448,6 +451,7 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
### Create project for user
@@ -473,6 +477,7 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
### Edit project
@@ -499,6 +504,7 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -529,7 +535,7 @@ POST /projects/:id/star
| `id` | integer | yes | The ID of the project |
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
```
Example response:
@@ -577,7 +583,8 @@ Example response:
"forks_count": 0,
"star_count": 1,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -595,7 +602,7 @@ DELETE /projects/:id/star
| `id` | integer | yes | The ID of the project |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
```
Example response:
@@ -643,7 +650,8 @@ Example response:
"forks_count": 0,
"star_count": 0,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -665,7 +673,7 @@ POST /projects/:id/archive
| `id` | integer | yes | The ID of the project |
```bash
-curl -X POST -H "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/archive"
```
Example response:
@@ -729,7 +737,8 @@ Example response:
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -751,7 +760,7 @@ POST /projects/:id/unarchive
| `id` | integer | yes | The ID of the project |
```bash
-curl -X POST -H "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/unarchive"
```
Example response:
@@ -815,7 +824,8 @@ Example response:
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -850,7 +860,6 @@ Parameters:
{
"alt": "dk",
"url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
- "is_image": true,
"markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"
}
```
@@ -859,95 +868,9 @@ Parameters:
In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
-## Team members
-
-### List project team members
-
-Get a list of a project's team members.
-
-```
-GET /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `query` (optional) - Query string to search for members
-
-### Get project team member
-
-Gets a project team member.
-
-```
-GET /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user
-
-```json
-{
- "id": 1,
- "username": "john_smith",
- "email": "john@example.com",
- "name": "John Smith",
- "state": "active",
- "created_at": "2012-05-23T08:00:58Z",
- "access_level": 40
-}
-```
-
-### Add project team member
-
-Adds a user to a project team. This is an idempotent method and can be called multiple times
-with the same parameters. Adding team membership to a user that is already a member does not
-affect the existing membership.
-
-```
-POST /projects/:id/members
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a user to add
-- `access_level` (required) - Project access level
-
-### Edit project team member
-
-Updates a project team member to a specified access level.
-
-```
-PUT /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
-- `access_level` (required) - Project access level
-
-### Remove project team member
-
-Removes a user from a project team.
-
-```
-DELETE /projects/:id/members/:user_id
-```
-
-Parameters:
-
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `user_id` (required) - The ID of a team member
+## Project members
-This method removes the project member if the user has the proper access rights to do so.
-It returns a status code 403 if the member does not have the proper rights to perform this action.
-In all other cases this method is idempotent and revoking team membership for a user who is not
-currently a team member is considered success.
-Please note that the returned JSON currently differs slightly. Thus you should not
-rely on the returned JSON structure.
+Please consult the [Project Members](members.md) documentation.
### Share project with group
@@ -1001,7 +924,11 @@ Parameters:
"push_events": true,
"issues_events": true,
"merge_requests_events": true,
+ "tag_push_events": true,
"note_events": true,
+ "build_events": true,
+ "pipeline_events": true,
+ "wiki_page_events": true,
"enable_ssl_verification": true,
"created_at": "2012-10-12T17:04:47Z"
}
@@ -1024,6 +951,9 @@ Parameters:
- `merge_requests_events` - Trigger hook on merge_requests events
- `tag_push_events` - Trigger hook on push_tag events
- `note_events` - Trigger hook on note events
+- `build_events` - Trigger hook on build events
+- `pipeline_events` - Trigger hook on pipeline events
+- `wiki_page_events` - Trigger hook on wiki page events
- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Edit project hook
@@ -1044,6 +974,9 @@ Parameters:
- `merge_requests_events` - Trigger hook on merge_requests events
- `tag_push_events` - Trigger hook on push_tag events
- `note_events` - Trigger hook on note events
+- `build_events` - Trigger hook on build events
+- `pipeline_events` - Trigger hook on pipeline events
+- `wiki_page_events` - Trigger hook on wiki page events
- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Delete project hook
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 623063f357b..fc3af5544de 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -12,6 +12,10 @@ Allows you to receive information about file in repository like name, size, cont
GET /projects/:id/repository/files
```
+```bash
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/models/key.rb&ref=master'
+```
+
Example response:
```json
@@ -39,6 +43,10 @@ Parameters:
POST /projects/:id/repository/files
```
+```bash
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20content&commit_message=create%20a%20new%20file'
+```
+
Example response:
```json
@@ -62,6 +70,10 @@ Parameters:
PUT /projects/:id/repository/files
```
+```bash
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&content=some%20other%20content&commit_message=update%20file'
+```
+
Example response:
```json
@@ -94,6 +106,10 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
DELETE /projects/:id/repository/files
```
+```bash
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v3/projects/13083/repository/files?file_path=app/project.rb&branch_name=master&commit_message=delete%20file'
+```
+
Example response:
```json
diff --git a/doc/api/runners.md b/doc/api/runners.md
index ddfa298f79d..28610762dca 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -18,7 +18,7 @@ GET /runners?scope=active
| `scope` | string | no | The scope of specific runners to show, one of: `active`, `paused`, `online`; showing all runners if none provided |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners"
```
Example response:
@@ -57,7 +57,7 @@ GET /runners/all?scope=online
| `scope` | string | no | The scope of runners to show, one of: `specific`, `shared`, `active`, `paused`, `online`; showing all runners if none provided |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/all"
```
Example response:
@@ -108,7 +108,7 @@ GET /runners/:id
| `id` | integer | yes | The ID of a runner |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
```
Example response:
@@ -158,7 +158,7 @@ PUT /runners/:id
| `tag_list` | array | no | The list of tags for a runner; put array of tags, that should be finally assigned to a runner |
```
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" -F "description=test-1-20150125-test" -F "tag_list=ruby,mysql,tag1,tag2"
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
Example response:
@@ -207,7 +207,7 @@ DELETE /runners/:id
| `id` | integer | yes | The ID of a runner |
```
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/runners/6"
```
Example response:
@@ -237,7 +237,7 @@ GET /projects/:id/runners
| `id` | integer | yes | The ID of a project |
```
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners"
```
Example response:
@@ -275,7 +275,7 @@ POST /projects/:id/runners
| `runner_id` | integer | yes | The ID of a runner |
```
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" -F "runner_id=9"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners" --form "runner_id=9"
```
Example response:
@@ -306,7 +306,7 @@ DELETE /projects/:id/runners/:runner_id
| `runner_id` | integer | yes | The ID of a runner |
```
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/9/runners/9"
```
Example response:
diff --git a/doc/api/services.md b/doc/api/services.md
index f821a614047..579fdc0c8c9 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -355,7 +355,7 @@ PUT /projects/:id/services/gemnasium
Parameters:
-- `api_key` (**required**) - Your personal API KEY on gemnasium.com
+- `api_key` (**required**) - Your personal API KEY on gemnasium.com
- `token` (**required**) - The project's slug on gemnasium.com
### Delete Gemnasium service
@@ -503,6 +503,7 @@ PUT /projects/:id/services/pivotaltracker
Parameters:
- `token` (**required**)
+- `restrict_to_branch` (optional) - Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.
### Delete PivotalTracker service
@@ -661,4 +662,3 @@ Get JetBrains TeamCity CI service settings for a project.
```
GET /projects/:id/services/teamcity
```
-
diff --git a/doc/api/session.md b/doc/api/session.md
index 066a055702d..f776424023e 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -2,7 +2,7 @@
## Deprecation Notice
-1. Starting in GitLab 9.0, this feature will be *disabled* for users with two-factor authentication turned on.
+1. Starting in GitLab 8.11, this feature has been *disabled* for users with two-factor authentication turned on.
2. These users can access the API using [personal access tokens] instead.
---
@@ -21,7 +21,7 @@ POST /session
| `password` | string | yes | The password of the user |
```bash
-curl -X POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
+curl --request POST "https://gitlab.example.com/api/v3/session?login=john_smith&password=strongpassw0rd"
```
Example response:
diff --git a/doc/api/settings.md b/doc/api/settings.md
index d9b68eaeadf..a76dad0ebd4 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -13,7 +13,7 @@ GET /application/settings
```
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings
```
Example response:
@@ -33,7 +33,9 @@ Example response:
"session_expire_delay" : 10080,
"home_page_url" : null,
"default_snippet_visibility" : 0,
- "restricted_signup_domains" : [],
+ "domain_whitelist" : [],
+ "domain_blacklist_enabled" : false,
+ "domain_blacklist" : [],
"created_at" : "2016-01-04T15:44:55.176Z",
"default_project_visibility" : 0,
"gravatar_enabled" : true,
@@ -63,7 +65,9 @@ PUT /application/settings
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes |
| `default_project_visibility` | integer | no | What visibility level new projects receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
| `default_snippet_visibility` | integer | no | What visibility level new snippets receive. Can take `0` _(Private)_, `1` _(Internal)_ and `2` _(Public)_ as a parameter. Default is `0`.|
-| `restricted_signup_domains` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
+| `domain_whitelist` | array of strings | no | Force people to use only corporate emails for sign-up. Default is null, meaning there is no restriction. |
+| `domain_blacklist_enabled` | boolean | no | Enable/disable the `domain_blacklist` |
+| `domain_blacklist` | array of strings | yes (if `domain_whitelist_enabled` is `true` | People trying to sign-up with emails from this domain will not be allowed to do so. |
| `user_oauth_applications` | boolean | no | Allow users to register any application to use GitLab as an OAuth provider |
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
@@ -71,7 +75,7 @@ PUT /application/settings
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
```
Example response:
@@ -93,7 +97,9 @@ Example response:
"session_expire_delay": 10080,
"default_project_visibility": 1,
"default_snippet_visibility": 0,
- "restricted_signup_domains": [],
+ "domain_whitelist": [],
+ "domain_blacklist_enabled" : false,
+ "domain_blacklist" : [],
"user_oauth_applications": true,
"after_sign_out_path": "",
"container_registry_token_expire_delay": 5,
diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md
index ebd131c94ca..1ae732d40d6 100644
--- a/doc/api/sidekiq_metrics.md
+++ b/doc/api/sidekiq_metrics.md
@@ -15,7 +15,7 @@ GET /sidekiq/queue_metrics
```
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/queue_metrics
```
Example response:
@@ -40,7 +40,7 @@ GET /sidekiq/process_metrics
```
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/process_metrics
```
Example response:
@@ -82,7 +82,7 @@ GET /sidekiq/job_stats
```
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/job_stats
```
Example response:
@@ -106,7 +106,7 @@ GET /sidekiq/compound_metrics
```
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/sidekiq/compound_metrics
```
Example response:
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index dc036d7e27f..1802fae14fe 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -20,7 +20,7 @@ GET /hooks
Example request:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks
```
Example response:
@@ -52,7 +52,7 @@ POST /hooks
Example request:
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/hooks?url=https://gitlab.example.com/hook"
```
Example response:
@@ -80,7 +80,7 @@ GET /hooks/:id
Example request:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
```
Example response:
@@ -117,7 +117,7 @@ DELETE /hooks/:id
Example request:
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/hooks/2
```
Example response:
diff --git a/doc/api/tags.md b/doc/api/tags.md
index ac9fac92f4c..54059117456 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -56,7 +56,7 @@ Parameters:
| `tag_name` | string | yes | The name of the tag |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/repository/tags/v1.0.0
```
Example Response:
diff --git a/doc/api/todos.md b/doc/api/todos.md
index 29e73664410..0cd644dfd2f 100644
--- a/doc/api/todos.md
+++ b/doc/api/todos.md
@@ -1,6 +1,6 @@
# Todos
-**Note:** This feature was [introduced][ce-3188] in GitLab 8.10
+> [Introduced][ce-3188] in GitLab 8.10.
## Get a list of todos
@@ -15,14 +15,14 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, or `marked`. |
+| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, `marked`, or `approval_required`. |
| `author_id` | integer | no | The ID of an author |
| `project_id` | integer | no | The ID of a project |
| `state` | string | no | The state of the todo. Can be either `pending` or `done` |
| `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
```
Example Response:
@@ -194,7 +194,7 @@ Parameters:
| `id` | integer | yes | The ID of a todo |
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
```
Example Response:
@@ -277,168 +277,20 @@ Example Response:
## Mark all todos as done
-Marks all pending todos for the current user as done. All todos marked as done
-are returned in the response.
+Marks all pending todos for the current user as done. It returns the number of marked todos.
```
DELETE /todos
```
```bash
-curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
```
Example Response:
```json
-[
- {
- "id": 102,
- "project": {
- "id": 2,
- "name": "Gitlab Ce",
- "name_with_namespace": "Gitlab Org / Gitlab Ce",
- "path": "gitlab-ce",
- "path_with_namespace": "gitlab-org/gitlab-ce"
- },
- "author": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
- },
- "action_name": "marked",
- "target_type": "MergeRequest",
- "target": {
- "id": 34,
- "iid": 7,
- "project_id": 2,
- "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
- "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
- "state": "opened",
- "created_at": "2016-06-17T07:49:24.419Z",
- "updated_at": "2016-06-17T07:52:43.484Z",
- "target_branch": "tutorials_git_tricks",
- "source_branch": "DNSBL_docs",
- "upvotes": 0,
- "downvotes": 0,
- "author": {
- "name": "Maxie Medhurst",
- "username": "craig_rutherford",
- "id": 12,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
- },
- "assignee": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
- },
- "source_project_id": 2,
- "target_project_id": 2,
- "labels": [],
- "work_in_progress": false,
- "milestone": {
- "id": 32,
- "iid": 2,
- "project_id": 2,
- "title": "v1.0",
- "description": "Assumenda placeat ea voluptatem voluptate qui.",
- "state": "active",
- "created_at": "2016-06-17T07:47:34.163Z",
- "updated_at": "2016-06-17T07:47:34.163Z",
- "due_date": null
- },
- "merge_when_build_succeeds": false,
- "merge_status": "cannot_be_merged",
- "subscribed": true,
- "user_notes_count": 7
- },
- "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
- "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
- "state": "done",
- "created_at": "2016-06-17T07:52:35.225Z"
- },
- {
- "id": 98,
- "project": {
- "id": 2,
- "name": "Gitlab Ce",
- "name_with_namespace": "Gitlab Org / Gitlab Ce",
- "path": "gitlab-ce",
- "path_with_namespace": "gitlab-org/gitlab-ce"
- },
- "author": {
- "name": "Maxie Medhurst",
- "username": "craig_rutherford",
- "id": 12,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
- },
- "action_name": "assigned",
- "target_type": "MergeRequest",
- "target": {
- "id": 34,
- "iid": 7,
- "project_id": 2,
- "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
- "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
- "state": "opened",
- "created_at": "2016-06-17T07:49:24.419Z",
- "updated_at": "2016-06-17T07:52:43.484Z",
- "target_branch": "tutorials_git_tricks",
- "source_branch": "DNSBL_docs",
- "upvotes": 0,
- "downvotes": 0,
- "author": {
- "name": "Maxie Medhurst",
- "username": "craig_rutherford",
- "id": 12,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/craig_rutherford"
- },
- "assignee": {
- "name": "Administrator",
- "username": "root",
- "id": 1,
- "state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
- "web_url": "https://gitlab.example.com/u/root"
- },
- "source_project_id": 2,
- "target_project_id": 2,
- "labels": [],
- "work_in_progress": false,
- "milestone": {
- "id": 32,
- "iid": 2,
- "project_id": 2,
- "title": "v1.0",
- "description": "Assumenda placeat ea voluptatem voluptate qui.",
- "state": "active",
- "created_at": "2016-06-17T07:47:34.163Z",
- "updated_at": "2016-06-17T07:47:34.163Z",
- "due_date": null
- },
- "merge_when_build_succeeds": false,
- "merge_status": "cannot_be_merged",
- "subscribed": true,
- "user_notes_count": 7
- },
- "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
- "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
- "state": "done",
- "created_at": "2016-06-17T07:49:24.624Z"
- },
-]
+3
```
[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
diff --git a/doc/ci/README.md b/doc/ci/README.md
index a9d407528e8..10ce4ac8940 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -14,7 +14,7 @@
- [Use variables in your `.gitlab-ci.yml`](variables/README.md)
- [Use SSH keys in your build environment](ssh_keys/README.md)
- [Trigger builds through the API](triggers/README.md)
-- [Build artifacts](build_artifacts/README.md)
-- [User permissions](permissions/README.md)
+- [Build artifacts](../user/project/builds/artifacts.md)
+- [User permissions](../user/permissions.md#gitlab-ci)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
diff --git a/doc/ci/build_artifacts/README.md b/doc/ci/build_artifacts/README.md
index 9553bb11e9d..05605f10fb4 100644
--- a/doc/ci/build_artifacts/README.md
+++ b/doc/ci/build_artifacts/README.md
@@ -1,175 +1,4 @@
-# Introduction to build artifacts
+This document was moved to:
-Artifacts is a list of files and directories which are attached to a build
-after it completes successfully. This feature is enabled by default in all GitLab installations.
-
-_If you are searching for ways to use artifacts, jump to
-[Defining artifacts in `.gitlab-ci.yml`](#defining-artifacts-in-gitlab-ciyml)._
-
-Since GitLab 8.2 and [GitLab Runner] 0.7.0, build artifacts that are created by
-GitLab Runner are uploaded to GitLab and are downloadable as a single archive
-(`tar.gz`) using the GitLab UI.
-
-Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
-changed to `ZIP`, and it is now possible to browse its contents, with the added
-ability of downloading the files separately.
-
-**Note:**
-The artifacts browser will be available only for new artifacts that are sent
-to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
-browse old artifacts already uploaded to GitLab.
-
-## Disabling build artifacts
-
-To disable artifacts site-wide, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following line:
-
- ```ruby
- gitlab_rails['artifacts_enabled'] = false
- ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
- ```yaml
- artifacts:
- enabled: false
- ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Defining artifacts in `.gitlab-ci.yml`
-
-A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
-the following:
-
-```yaml
-pdf:
- script: xelatex mycv.tex
- artifacts:
- paths:
- - mycv.pdf
-```
-
-A job named `pdf` calls the `xelatex` command in order to build a pdf file from
-the latex source file `mycv.tex`. We then define the `artifacts` paths which in
-turn are defined with the `paths` keyword. All paths to files and directories
-are relative to the repository that was cloned during the build.
-
-For more examples on artifacts, follow the
-[separate artifacts yaml documentation](../yaml/README.md#artifacts).
-
-## Storing build artifacts
-
-After a successful build, GitLab Runner uploads an archive containing the build
-artifacts to GitLab.
-
-To change the location where the artifacts are stored, follow the steps below.
-
----
-
-**In Omnibus installations:**
-
-_The artifacts are stored by default in
-`/var/opt/gitlab/gitlab-rails/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
- `/etc/gitlab/gitlab.rb` and add the following line:
-
- ```ruby
- gitlab_rails['artifacts_path'] = "/mnt/storage/artifacts"
- ```
-
-1. Save the file and [reconfigure GitLab][] for the changes to take effect.
-
----
-
-**In installations from source:**
-
-_The artifacts are stored by default in
-`/home/git/gitlab/shared/artifacts`._
-
-1. To change the storage path for example to `/mnt/storage/artifacts`, edit
- `/home/git/gitlab/config/gitlab.yml` and add or amend the following lines:
-
- ```yaml
- artifacts:
- enabled: true
- path: /mnt/storage/artifacts
- ```
-
-1. Save the file and [restart GitLab][] for the changes to take effect.
-
-## Browsing build artifacts
-
-When GitLab receives an artifacts archive, an archive metadata file is also
-generated. This metadata file describes all the entries that are located in the
-artifacts archive itself. The metadata file is in a binary format, with
-additional GZIP compression.
-
-GitLab does not extract the artifacts archive in order to save space, memory
-and disk I/O. It instead inspects the metadata file which contains all the
-relevant information. This is especially important when there is a lot of
-artifacts, or an archive is a very large file.
-
----
-
-After a successful build, if you visit the build's specific page, you can see
-that there are two buttons.
-
-One is for downloading the artifacts archive and the other for browsing its
-contents.
-
-![Build artifacts browser button](img/build_artifacts_browser_button.png)
-
----
-
-The archive browser shows the name and the actual file size of each file in the
-archive. If your artifacts contained directories, then you are also able to
-browse inside them.
-
-Below you can see an image of three different file formats, as well as two
-directories.
-
-![Build artifacts browser](img/build_artifacts_browser.png)
-
----
-
-## Downloading build artifacts
-
-If you need to download the whole archive, there are buttons in various places
-inside GitLab that make that possible.
-
-1. While on the builds page, you can see the download icon for each build's
- artifacts archive in the right corner
-
-1. While inside a specific build, you are presented with a download button
- along with the one that browses the archive
-
-1. And finally, when browsing an archive you can see the download button at
- the top right corner
-
----
-
-Note that GitLab does not extract the entire artifacts archive to send just a
-single file to the user.
-
-When clicking on a specific file, [GitLab Workhorse] extracts it from the
-archive and the download begins.
-
-This implementation saves space, memory and disk I/O.
-
-[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner "GitLab Runner repository"
-[reconfigure gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
-[restart gitlab]: ../../administration/restart_gitlab.md "How to restart GitLab documentation"
-[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
+- [user/project/builds/artifacts.md](../../user/project/builds/artifacts.md) - user guide
+- [administration/build_artifacts.md](../../administration/build_artifacts.md) - administrator guide
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser.png b/doc/ci/build_artifacts/img/build_artifacts_browser.png
deleted file mode 100644
index 59cf2b8746b..00000000000
--- a/doc/ci/build_artifacts/img/build_artifacts_browser.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png b/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
deleted file mode 100644
index 7801c2e6fa6..00000000000
--- a/doc/ci/build_artifacts/img/build_artifacts_browser_button.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 7f83f846454..0f64137a8a9 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -38,7 +38,7 @@ GitLab Runner then executes build scripts as the `gitlab-runner` user.
$ sudo gitlab-ci-multi-runner register -n \
--url https://gitlab.com/ci \
--registration-token REGISTRATION_TOKEN \
- --executor shell
+ --executor shell \
--description "My Runner"
```
diff --git a/doc/ci/examples/php.md b/doc/ci/examples/php.md
index bfafcc44d66..175e9d79904 100644
--- a/doc/ci/examples/php.md
+++ b/doc/ci/examples/php.md
@@ -49,7 +49,7 @@ apt-get update -yqq
apt-get install git -yqq
# Install phpunit, the tool that we will use for testing
-curl -Lo /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
+curl --location --output /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
chmod +x /usr/local/bin/phpunit
# Install mysql driver
@@ -235,7 +235,7 @@ cache:
before_script:
# Install composer dependencies
-- curl -sS https://getcomposer.org/installer | php
+- curl --silent --show-error https://getcomposer.org/installer | php
- php composer.phar install
...
diff --git a/doc/ci/permissions/README.md b/doc/ci/permissions/README.md
index d77061c14cd..42eb59f84c8 100644
--- a/doc/ci/permissions/README.md
+++ b/doc/ci/permissions/README.md
@@ -1,24 +1,3 @@
# Users Permissions
-GitLab CI relies on user's role on the GitLab. There are three permissions levels on GitLab CI: admin, master, developer, other.
-
-Admin user can perform any actions on GitLab CI in scope of instance and project. Also user with admin permission can use admin interface.
-
-
-
-
-| Action | Guest, Reporter | Developer | Master | Admin |
-|---------------------------------------|-----------------|-------------|----------|--------|
-| See commits and builds | ✓ | ✓ | ✓ | ✓ |
-| Retry or cancel build | | ✓ | ✓ | ✓ |
-| Remove project | | | ✓ | ✓ |
-| Create project | | | ✓ | ✓ |
-| Change project configuration | | | ✓ | ✓ |
-| Add specific runners | | | ✓ | ✓ |
-| Add shared runners | | | | ✓ |
-| See events in the system | | | | ✓ |
-| Admin interface | | | | ✓ |
-
-
-
-
+This document was moved to [user/permissions.md](../../user/permissions.md#gitlab-ci).
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 48a9f994759..ca9b986a060 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -5,7 +5,7 @@ Introduced in GitLab 8.8.
## Pipelines
-A pipeline is a group of [builds] that get executed in [stages] (batches). All
+A pipeline is a group of [builds] that get executed in [stages] \(batches). All
of the builds in a stage are executed in parallel (if there are enough
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)
@@ -32,6 +32,43 @@ project.
Clicking on a pipeline will show the builds that were run for that pipeline.
+## Badges
+
+There are build status and test coverage report badges available.
+
+Go to pipeline settings to see available badges and code you can use to embed
+badges in the `README.md` or your website.
+
+### Build status badge
+
+You can access a build status badge image using following link:
+
+```
+http://example.gitlab.com/namespace/project/badges/branch/build.svg
+```
+
+### Test coverage report badge
+
+GitLab makes it possible to define the regular expression for coverage report,
+that each build log will be matched against. This means that each build in the
+pipeline can have the test coverage percentage value defined.
+
+You can access test coverage badge using following link:
+
+```
+http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
+```
+
+If you would like to get the coverage report from the specific job, you can add
+a `job=coverage_job_name` parameter to the URL. For example, it is possible to
+use following Markdown code to embed the est coverage report into `README.md`:
+
+```markdown
+![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
+```
+
+The latest successful pipeline will be used to read the test coverage value.
+
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 7fa1a478f34..c835ebc2d44 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -218,22 +218,14 @@ project's settings.
For more information read the
[Builds emails service documentation](../../project_services/builds_emails.md).
-## Builds badge
-
-You can access a builds badge image using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/build.svg
-```
-
-Awesome! You started using CI in GitLab!
-
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
CI with various languages.
-[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
+Awesome! You started using CI in GitLab!
+
+[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#install-gitlab-runner
[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
[examples]: ../examples/README.md
[ci]: https://about.gitlab.com/gitlab-ci/
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 5c316510d0e..6c6767fea0b 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,6 +1,6 @@
# Triggering Builds through the API
-_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
+> [Introduced][ci-229] in GitLab CE 7.14.
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
@@ -77,9 +77,9 @@ See the [Examples](#examples) section below for more details.
Using cURL you can trigger a rebuild with minimal effort, for example:
```bash
-curl -X POST \
- -F token=TOKEN \
- -F ref=master \
+curl --request POST \
+ --form token=TOKEN \
+ --form ref=master \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
@@ -88,7 +88,7 @@ In this case, the project with ID `9` will get rebuilt on `master` branch.
Alternatively, you can pass the `token` and `ref` arguments in the query string:
```bash
-curl -X POST \
+curl --request POST \
"https://gitlab.example.com/api/v3/projects/9/trigger/builds?token=TOKEN&ref=master"
```
@@ -103,7 +103,7 @@ need to add in project's A `.gitlab-ci.yml`:
build_docs:
stage: deploy
script:
- - "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
+ - "curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
only:
- tags
```
@@ -158,10 +158,10 @@ You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
and the script of the `upload_package` job will run:
```bash
-curl -X POST \
- -F token=TOKEN \
- -F ref=master \
- -F "variables[UPLOAD_TO_S3]=true" \
+curl --request POST \
+ --form token=TOKEN \
+ --form ref=master \
+ --form "variables[UPLOAD_TO_S3]=true" \
https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
@@ -172,7 +172,7 @@ in conjunction with cron. The example below triggers a build on the `master`
branch of project with ID `9` every night at `00:30`:
```bash
-30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
+30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 137b080a8f7..4a7c21f811d 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -18,25 +18,35 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
### Predefined variables (Environment Variables)
-| Variable | Runner | Description |
-|-------------------------|-----|--------|
-| **CI** | 0.4 | Mark that build is executed in CI environment |
-| **GITLAB_CI** | all | Mark that build is executed in GitLab CI environment |
-| **CI_SERVER** | all | Mark that build is executed in CI environment |
-| **CI_SERVER_NAME** | all | CI server that is used to coordinate builds |
-| **CI_SERVER_VERSION** | all | Not yet defined |
-| **CI_SERVER_REVISION** | all | Not yet defined |
-| **CI_BUILD_REF** | all | The commit revision for which project is built |
-| **CI_BUILD_TAG** | 0.5 | The commit tag name. Present only when building tags. |
-| **CI_BUILD_NAME** | 0.5 | The name of the build as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_STAGE** | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built |
-| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally |
-| **CI_BUILD_REPO** | all | The URL to clone the Git repository |
-| **CI_BUILD_TRIGGERED** | 0.5 | The flag to indicate that build was [triggered] |
-| **CI_BUILD_TOKEN** | 1.2 | Token used for authenticating with the GitLab Container Registry |
-| **CI_PROJECT_ID** | all | The unique id of the current project that GitLab CI uses internally |
-| **CI_PROJECT_DIR** | all | The full path where the repository is cloned and where the build is ran |
+| Variable | GitLab | Runner | Description |
+|-------------------------|--------|--------|-------------|
+| **CI** | all | 0.4 | Mark that build is executed in CI environment |
+| **GITLAB_CI** | all | all | Mark that build is executed in GitLab CI environment |
+| **CI_SERVER** | all | all | Mark that build is executed in CI environment |
+| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate builds |
+| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule builds |
+| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule builds |
+| **CI_BUILD_ID** | all | all | The unique id of the current build that GitLab CI uses internally |
+| **CI_BUILD_REF** | all | all | The commit revision for which project is built |
+| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. |
+| **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_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_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry |
+| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
+| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built |
+| **CI_PROJECT_NAMESPACE**| 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
+| **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_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 returnes the address of the registry tied to the specific project |
+| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
+| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
+| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
**Some of the variables are only available when using runner with at least defined version.**
@@ -46,18 +56,28 @@ Example values:
export CI_BUILD_ID="50"
export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_BUILD_REF_NAME="master"
-export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git"
+export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@gitlab.com/gitlab-org/gitlab-ce.git"
export CI_BUILD_TAG="1.0.0"
export CI_BUILD_NAME="spec:other"
export CI_BUILD_STAGE="test"
export CI_BUILD_TRIGGERED="true"
export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
-export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
+export CI_PIPELINE_ID="1000"
export CI_PROJECT_ID="34"
+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_RUNNER_ID="10"
+export CI_RUNNER_DESCRIPTION="my runner"
+export CI_RUNNER_TAGS="docker, linux"
export CI_SERVER="yes"
-export CI_SERVER_NAME="GitLab CI"
-export CI_SERVER_REVISION=""
-export CI_SERVER_VERSION=""
+export CI_SERVER_NAME="GitLab"
+export CI_SERVER_REVISION="8.9.0"
+export CI_SERVER_VERSION="70606bf"
```
### YAML-defined variables
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index eb81267242e..e7850aa2c9d 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -13,34 +13,36 @@ If you want a quick introduction to GitLab CI, follow our
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [.gitlab-ci.yml](#gitlab-ci-yml)
- - [image and services](#image-and-services)
- - [before_script](#before_script)
- - [after_script](#after_script)
- - [stages](#stages)
- - [types](#types)
- - [variables](#variables)
- - [cache](#cache)
- - [cache:key](#cache-key)
+ - [image and services](#image-and-services)
+ - [before_script](#before_script)
+ - [after_script](#after_script)
+ - [stages](#stages)
+ - [types](#types)
+ - [variables](#variables)
+ - [cache](#cache)
+ - [cache:key](#cache-key)
- [Jobs](#jobs)
- - [script](#script)
- - [stage](#stage)
- - [only and except](#only-and-except)
- - [job variables](#job-variables)
- - [tags](#tags)
- - [when](#when)
- - [environment](#environment)
- - [artifacts](#artifacts)
- - [artifacts:name](#artifactsname)
- - [artifacts:when](#artifactswhen)
- - [artifacts:expire_in](#artifactsexpire_in)
- - [dependencies](#dependencies)
- - [before_script and after_script](#before_script-and-after_script)
+ - [script](#script)
+ - [stage](#stage)
+ - [only and except](#only-and-except)
+ - [job variables](#job-variables)
+ - [tags](#tags)
+ - [allow_failure](#allow_failure)
+ - [when](#when)
+ - [Manual actions](#manual-actions)
+ - [environment](#environment)
+ - [artifacts](#artifacts)
+ - [artifacts:name](#artifacts-name)
+ - [artifacts:when](#artifacts-when)
+ - [artifacts:expire_in](#artifacts-expire_in)
+ - [dependencies](#dependencies)
+ - [before_script and after_script](#before_script-and-after_script)
- [Git Strategy](#git-strategy)
- [Shallow cloning](#shallow-cloning)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ciyml)
+ - [Anchors](#anchors)
+- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
- [Skipping builds](#skipping-builds)
- [Examples](#examples)
@@ -133,7 +135,7 @@ builds, including deploy builds. This can be an array or a multi-line string.
### after_script
>**Note:**
-Introduced in GitLab 8.7 and requires Gitlab Runner v1.2 (not yet released)
+Introduced in GitLab 8.7 and requires Gitlab Runner v1.2
`after_script` is used to define the command that will be run after for all
builds. This has to be an array or a multi-line string.
@@ -351,7 +353,7 @@ job_name:
| except | no | Defines a list of git refs for which build is not created |
| tags | no | Defines a list of tags which are used to select Runner |
| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
-| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| when | no | Define when to run build. Can be `on_success`, `on_failure`, `always` or `manual` |
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list of build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -377,6 +379,8 @@ job:
- bundle exec rspec
```
+Sometimes, `script` commands will need to be wrapped in single or double quotes. For example, commands that contain a colon (`:`) need to be wrapped in quotes so that the YAML parser knows to interpret the whole thing as a string rather than a "key: value" pair. Be careful when using special characters (`:`, `{`, `}`, `[`, `]`, `,`, `&`, `*`, `#`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`, `` ` ``).
+
### stage
`stage` allows to group build into different stages. Builds of the same `stage`
@@ -473,6 +477,39 @@ job:
The specification above, will make sure that `job` is built by a Runner that
has both `ruby` AND `postgres` tags defined.
+### allow_failure
+
+`allow_failure` is used when you want to allow a build to fail without impacting
+the rest of the CI suite. Failed builds don't contribute to the commit status.
+
+When enabled and the build fails, the pipeline will be successful/green for all
+intents and purposes, but a "CI build passed with warnings" message will be
+displayed on the merge request or commit or build page. This is to be used by
+builds that are allowed to fail, but where failure indicates some other (manual)
+steps should be taken elsewhere.
+
+In the example below, `job1` and `job2` will run in parallel, but if `job1`
+fails, it will not stop the next stage from running, since it's marked with
+`allow_failure: true`:
+
+```yaml
+job1:
+ stage: test
+ script:
+ - execute_script_that_will_fail
+ allow_failure: true
+
+job2:
+ stage: test
+ script:
+ - execute_script_that_will_succeed
+
+job3:
+ stage: deploy
+ script:
+ - deploy_to_staging
+```
+
### when
`when` is used to implement jobs that are run in case of failure or despite the
@@ -485,6 +522,8 @@ failure.
1. `on_failure` - execute build only when at least one build from prior stages
fails.
1. `always` - execute build regardless of the status of builds from prior stages.
+1. `manual` - execute build manually (added in GitLab 8.10). Read about
+ [manual actions](#manual-actions) below.
For example:
@@ -516,6 +555,7 @@ deploy_job:
stage: deploy
script:
- make deploy
+ when: manual
cleanup_job:
stage: cleanup
@@ -526,8 +566,22 @@ cleanup_job:
The above script will:
-1. Execute `cleanup_build_job` only when `build_job` fails
-2. Always execute `cleanup_job` as the last step in pipeline.
+1. Execute `cleanup_build_job` only when `build_job` fails.
+2. Always execute `cleanup_job` as the last step in pipeline regardless of
+ success or failure.
+3. Allow you to manually execute `deploy_job` from GitLab's UI.
+
+#### Manual actions
+
+>**Note:**
+Introduced in GitLab 8.10.
+
+Manual actions are a special type of job that are not executed automatically;
+they need to be explicitly started by a user. Manual actions can be started
+from pipeline, build, environment, and deployment views. You can execute the
+same manual action multiple times.
+
+An example usage of manual actions is deployment to production.
### environment
@@ -630,9 +684,10 @@ be available for download in the GitLab UI.
Introduced in GitLab 8.6 and GitLab Runner v1.1.0.
The `name` directive allows you to define the name of the created artifacts
-archive. That way, you can have a unique name of every archive which could be
+archive. That way, you can have a unique name for every archive which could be
useful when you'd like to download the archive from GitLab. The `artifacts:name`
variable can make use of any of the [predefined variables](../variables/README.md).
+The default name is `artifacts`, which becomes `artifacts.zip` when downloaded.
---
@@ -757,12 +812,13 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
This feature should be used in conjunction with [`artifacts`](#artifacts) and
allows you to define the artifacts to pass between different builds.
-Note that `artifacts` from previous [stages](#stages) are passed by default.
+Note that `artifacts` from all previous [stages](#stages) are passed by default.
To use this feature, define `dependencies` in context of the job and pass
a list of all previous builds from which the artifacts should be downloaded.
You can only define builds from stages that are executed before the current one.
An error will be shown if you define builds from the current stage or next ones.
+Defining an empty array will skip downloading any artifacts for that job.
---
@@ -985,11 +1041,11 @@ directive defined in `.postgres_services` and `.mysql_services` respectively:
- ruby
test:postgres:
- << *job_definition
+ <<: *job_definition
services: *postgres_definition
test:mysql:
- << *job_definition
+ <<: *job_definition
services: *mysql_definition
```
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index 1b465434498..047a0b08406 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,7 +1,7 @@
# GitLab Container Registry
-> **Note:**
-This feature was [introduced][ce-4040] in GitLab 8.8.
+> [Introduced][ce-4040] in GitLab 8.8. Docker Registry manifest
+`v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10.
> **Note:**
This document is about the user guide. To learn how to enable GitLab Container
@@ -89,6 +89,10 @@ your `.gitlab-ci.yml`, you have to follow the
[Using a private Docker Registry][private-docker]
documentation. This workflow will be simplified in the future.
+## Troubleshooting
+
+See [the GitLab Docker registry troubleshooting guide](troubleshooting.md).
+
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/container_registry/img/mitmproxy-docker.png
new file mode 100644
index 00000000000..4e3e37b413d
--- /dev/null
+++ b/doc/container_registry/img/mitmproxy-docker.png
Binary files differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
new file mode 100644
index 00000000000..14c4a7d9a63
--- /dev/null
+++ b/doc/container_registry/troubleshooting.md
@@ -0,0 +1,141 @@
+# Troubleshooting the GitLab Container Registry
+
+## Basic Troubleshooting
+
+1. Check to make sure that the system clock on your Docker client and GitLab server have
+ been synchronized (e.g. via NTP).
+
+2. If you are using an S3-backed Registry, double check that the IAM
+ permissions and the S3 credentials (including region) are correct. See [the
+ sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
+ for more details.
+
+3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+ for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
+ there.
+
+## Advanced Troubleshooting
+
+>**NOTE:** The following section is only recommended for experts.
+
+Sometimes it's not obvious what is wrong, and you may need to dive deeper into
+the communication between the Docker client and the Registry to find out
+what's wrong. We will use a concrete example in the past to illustrate how to
+diagnose a problem with the S3 setup.
+
+### Unexpected 403 error during push
+
+A user attempted to enable an S3-backed Registry. The `docker login` step went
+fine. However, when pushing an image, the output showed:
+
+```
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+dc5e59c14160: Pushing [==================================================>] 14.85 kB
+03c20c1a019a: Pushing [==================================================>] 2.048 kB
+a08f14ef632e: Pushing [==================================================>] 2.048 kB
+228950524c88: Pushing 2.048 kB
+6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
+5f70bf18a086: Pushing 1.024 kB
+737f40e80b7f: Waiting
+82b57dbc5385: Waiting
+19429b698a22: Waiting
+9436069b92a3: Waiting
+error parsing HTTP 403 response body: unexpected end of JSON input: ""
+```
+
+This error is ambiguous, as it's not clear whether the 403 is coming from the
+GitLab Rails application, the Docker Registry, or something else. In this
+case, since we know that since the login succeeded, we probably need to look
+at the communication between the client and the Registry.
+
+The REST API between the Docker client and Registry is [described
+here](https://docs.docker.com/registry/spec/api/). Normally, one would just
+use Wireshark or tcpdump to capture the traffic and see where things went
+wrong. However, since all communication between Docker clients and servers
+are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
+if you know the private key. What can we do instead?
+
+One way would be to disable HTTPS by setting up an [insecure
+Registry](https://docs.docker.com/registry/insecure/). This could introduce a
+security hole and is only recommended for local testing. If you have a
+production system and can't or don't want to do this, there is another way:
+use mitmproxy, which stands for Man-in-the-Middle Proxy.
+
+### mitmproxy
+
+[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
+client and server to inspect all traffic. One wrinkle is that your system
+needs to trust the mitmproxy SSL certificates for this to work.
+
+The following installation instructions assume you are running Ubuntu:
+
+1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
+1. Run `mitmproxy --port 9000` to generate its certificates.
+ Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
+1. Install the certificate from `~/.mitmproxy` to your system:
+
+ ```sh
+ sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
+ sudo update-ca-certificates
+ ```
+
+If successful, the output should indicate that a certificate was added:
+
+```sh
+Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d....done.
+```
+
+To verify that the certificates are properly installed, run:
+
+```sh
+mitmproxy --port 9000
+```
+
+This will run mitmproxy on port `9000`. In another window, run:
+
+```sh
+curl --proxy http://localhost:9000 https://httpbin.org/status/200
+```
+
+If everything is setup correctly, you will see information on the mitmproxy window and
+no errors from the curl commands.
+
+### Running the Docker daemon with a proxy
+
+For Docker to connect through a proxy, you must start the Docker daemon with the
+proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
+and then run Docker by hand. As root, run:
+
+```sh
+export HTTP_PROXY="http://localhost:9000"
+export HTTPS_PROXY="https://localhost:9000"
+docker daemon --debug
+```
+
+This will launch the Docker daemon and proxy all connections through mitmproxy.
+
+### Running the Docker client
+
+Now that we have mitmproxy and Docker running, we can attempt to login and push
+a container image. You may need to run as root to do this. For example:
+
+```sh
+docker login s3-testing.myregistry.com:4567
+docker push s3-testing.myregistry.com:4567/root/docker-test
+```
+
+In the example above, we see the following trace on the mitmproxy window:
+
+![mitmproxy output from Docker](img/mitmproxy-docker.png)
+
+The above image shows:
+
+* The initial PUT requests went through fine with a 201 status code.
+* The 201 redirected the client to the S3 bucket.
+* The HEAD request to the AWS bucket reported a 403 Unauthorized.
+
+What does this mean? This strongly suggests that the S3 user does not have the right
+[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
+Once the right permissions were set, the error will go away.
diff --git a/doc/development/README.md b/doc/development/README.md
index c5d5af43864..57f37da6f80 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -1,18 +1,41 @@
# Development
+## Outside of docs
+
+- [CONTRIBUTING.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) main contributing guide
+- [PROCESS.md](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) contributing process
+- [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) to install a development version
+
+## Styleguides
+
+- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
+ 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
+- [SQL guidelines](sql.md) for SQL guidelines
+
+## Process
+
+- [Code review guidelines](code_review.md) for reviewing code and having code reviewed.
+
+## Backend howtos
+
- [Architecture](architecture.md) of GitLab
- [CI setup](ci_setup.md) for testing GitLab
-- [Code review guidelines](code_review.md) for reviewing code and having code
- reviewed.
- [Gotchas](gotchas.md) to avoid
- [How to dump production data to staging](db_dump.md)
- [Instrumentation](instrumentation.md)
-- [Licensing](licensing.md) for ensuring license compliance
-- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [Performance guidelines](performance.md)
- [Rake tasks](rake_tasks.md) for development
- [Shell commands](shell_commands.md) in the GitLab codebase
- [Sidekiq debugging](sidekiq_debugging.md)
-- [SQL guidelines](sql.md) for SQL guidelines
-- [Testing standards and style guidelines](testing.md)
-- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
+
+## Databases
+
+- [What requires downtime?](what_requires_downtime.md)
+- [Adding database indexes](adding_database_indexes.md)
+
+## Compliance
+
+- [Licensing](licensing.md) for ensuring license compliance
diff --git a/doc/development/adding_database_indexes.md b/doc/development/adding_database_indexes.md
new file mode 100644
index 00000000000..ea6f14da3b9
--- /dev/null
+++ b/doc/development/adding_database_indexes.md
@@ -0,0 +1,123 @@
+# Adding Database Indexes
+
+Indexes can be used to speed up database queries, but when should you add a new
+index? Traditionally the answer to this question has been to add an index for
+every column used for filtering or joining data. For example, consider the
+following query:
+
+```sql
+SELECT *
+FROM projects
+WHERE user_id = 2;
+```
+
+Here we are filtering by the `user_id` column and as such a developer may decide
+to index this column.
+
+While in certain cases indexing columns using the above approach may make sense
+it can actually have a negative impact. Whenever you write data to a table any
+existing indexes need to be updated. The more indexes there are the slower this
+can potentially become. Indexes can also take up quite some disk space depending
+on the amount of data indexed and the index type. For example, PostgreSQL offers
+"GIN" indexes which can be used to index certain data types that can not be
+indexed by regular btree indexes. These indexes however generally take up more
+data and are slower to update compared to btree indexes.
+
+Because of all this one should not blindly add a new index for every column used
+to filter data by. Instead one should ask themselves the following questions:
+
+1. Can I write my query in such a way that it re-uses as many existing indexes
+ as possible?
+2. Is the data going to be large enough that using an index will actually be
+ faster than just iterating over the rows in the table?
+3. Is the overhead of maintaining the index worth the reduction in query
+ timings?
+
+We'll explore every question in detail below.
+
+## Re-using Queries
+
+The first step is to make sure your query re-uses as many existing indexes as
+possible. For example, consider the following query:
+
+```sql
+SELECT *
+FROM todos
+WHERE user_id = 123
+AND state = 'open';
+```
+
+Now imagine we already have an index on the `user_id` column but not on the
+`state` column. One may think this query will perform badly due to `state` being
+unindexed. In reality the query may perform just fine given the index on
+`user_id` can filter out enough rows.
+
+The best way to determine if indexes are re-used is to run your query using
+`EXPLAIN ANALYZE`. Depending on any extra tables that may be joined and
+other columns being used for filtering you may find an extra index is not going
+to make much (if any) difference. On the other hand you may determine that the
+index _may_ make a difference.
+
+In short:
+
+1. Try to write your query in such a way that it re-uses as many existing
+ indexes as possible.
+2. Run the query using `EXPLAIN ANALYZE` and study the output to find the most
+ ideal query.
+
+## Data Size
+
+A database may decide not to use an index despite it existing in case a regular
+sequence scan (= simply iterating over all existing rows) is faster. This is
+especially the case for small tables.
+
+If a table is expected to grow in size and you expect your query has to filter
+out a lot of rows you may want to consider adding an index. If the table size is
+very small (e.g. only a handful of rows) or any existing indexes filter out
+enough rows you may _not_ want to add a new index.
+
+## Maintenance Overhead
+
+Indexes have to be updated on every table write. In case of PostgreSQL _all_
+existing indexes will be updated whenever data is written to a table. As a
+result of this having many indexes on the same table will slow down writes.
+
+Because of this one should ask themselves: is the reduction in query performance
+worth the overhead of maintaining an extra index?
+
+If adding an index reduces SELECT timings by 5 milliseconds but increases
+INSERT/UPDATE/DELETE timings by 10 milliseconds then the index may not be worth
+it. On the other hand, if SELECT timings are reduced but INSERT/UPDATE/DELETE
+timings are not affected you may want to add the index after all.
+
+## Finding Unused Indexes
+
+To see which indexes are unused you can run the following query:
+
+```sql
+SELECT relname as table_name, indexrelname as index_name, idx_scan, idx_tup_read, idx_tup_fetch, pg_size_pretty(pg_relation_size(indexrelname::regclass))
+FROM pg_stat_all_indexes
+WHERE schemaname = 'public'
+AND idx_scan = 0
+AND idx_tup_read = 0
+AND idx_tup_fetch = 0
+ORDER BY pg_relation_size(indexrelname::regclass) desc;
+```
+
+This query outputs a list containing all indexes that are never used and sorts
+them by indexes sizes in descending order. This query can be useful to
+determine if any previously indexes are useful after all. More information on
+the meaning of the various columns can be found at
+<https://www.postgresql.org/docs/current/static/monitoring-stats.html>.
+
+Because the output of this query relies on the actual usage of your database it
+may be affected by factors such as (but not limited to):
+
+* Certain queries never being executed, thus not being able to use certain
+ indexes.
+* Certain tables having little data, resulting in PostgreSQL using sequence
+ scans instead of index scans.
+
+In other words, this data is only reliable for a frequently used database with
+plenty of data and with as many GitLab features enabled (and being used) as
+possible.
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 975bb82c37d..37bb59e112c 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -3,12 +3,64 @@
This styleguide recommends best practices to improve documentation and to keep
it organized and easy to find.
-## Naming
+## Location and naming of documents
-- When creating a new document and it has more than one word in its name,
- make sure to use underscores instead of spaces or dashes (`-`). For example,
- a proper naming would be `import_projects_from_github.md`. The same rule
- applies to images.
+>**Note:**
+These guidelines derive from the discussion taken place in issue [#3349][ce-3349].
+
+The documentation hierarchy can be vastly improved by providing a better layout
+and organization of directories.
+
+Having a structured document layout, we will be able to have meaningful URLs
+like `docs.gitlab.com/user/project/merge_requests.html`. With this pattern,
+you can immediately tell that you are navigating a user related documentation
+and is about the project and its merge requests.
+
+The table below shows what kind of documentation goes where.
+
+| Directory | What belongs here |
+| --------- | -------------- |
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
+| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
+| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+
+---
+
+**General rules:**
+
+1. The correct naming and location of a new document, is a combination
+ of the relative URL of the document in question and the GitLab Map design
+ that is used for UX purposes ([source][graffle], [image][gitlab-map]).
+1. When creating a new document and it has more than one word in its name,
+ make sure to use underscores instead of spaces or dashes (`-`). For example,
+ a proper naming would be `import_projects_from_github.md`. The same rule
+ applies to images.
+1. There are four main directories, `user`, `administration`, `api` and `development`.
+1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`,
+ `profile/`, `dashboard/` and `admin_area/`.
+ 1. `doc/user/project/` should contain all project related documentation.
+ 1. `doc/user/group/` should contain all group related documentation.
+ 1. `doc/user/profile/` should contain all profile related documentation.
+ Every page you would navigate under `/profile` should have its own document,
+ i.e. `account.md`, `applications.md`, `emails.md`, etc.
+ 1. `doc/user/dashboard/` should contain all dashboard related documentation.
+ 1. `doc/user/admin_area/` should contain all admin related documentation
+ describing what can be achieved by accessing GitLab's admin interface
+ (_not to be confused with `doc/administration` where server access is
+ required_).
+ 1. Every category under `/admin/application_settings` should have its
+ own document located at `doc/user/admin_area/settings/`. For example,
+ the **Visibility and Access Controls** category should have a document
+ located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
+
+---
+
+If you are unsure where a document should live, you can ping `@axil` in your
+merge request.
## Text
@@ -44,7 +96,7 @@ it organized and easy to find.
- When introducing a new document, be careful for the headings to be
grammatically and syntactically correct. It is advised to mention one or all
of the following GitLab members for a review: `@axil`, `@rspeicher`,
- `@dblessing`, `@ashleys`, `@nearlythere`. This is to ensure that no document
+ `@dblessing`, `@ashleys`. This is to ensure that no document
with wrong heading is going live without an audit, thus preventing dead links
and redirection issues when corrected
- Leave exactly one newline after a heading
@@ -103,15 +155,15 @@ Inside the document:
- Every piece of documentation that comes with a new feature should declare the
GitLab version that feature got introduced. Right below the heading add a
- note: `>**Note:** This feature was introduced in GitLab 8.3`
+ note: `> Introduced in GitLab 8.3.`.
- If possible every feature should have a link to the MR that introduced it.
The above note would be then transformed to:
- `>**Note:** This feature was [introduced][ce-1242] in GitLab 8.3`, where
+ `> [Introduced][ce-1242] in GitLab 8.3.`, where
the [link identifier](#links) is named after the repository (CE) and the MR
- number
+ number.
- If the feature is only in GitLab EE, don't forget to mention it, like:
- `>**Note:** This feature was introduced in GitLab EE 8.3`. Otherwise, leave
- this mention out
+ `> Introduced in GitLab EE 8.3.`. Otherwise, leave
+ this mention out.
## References
@@ -170,18 +222,26 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
```
1. Find and replace any occurrences of the old location with the new one.
- A quick way to find them is to use `grep`:
+ A quick way to find them is to use `git grep`. First go to the root directory
+ where you cloned the `gitlab-ce` repository and then do:
```
- grep -nR "lfs_administration.md" doc/
+ git grep -n "workflow/lfs/lfs_administration"
+ git grep -n "lfs/lfs_administration"
```
- The above command will search in the `doc/` directory for
- `lfs_administration.md` recursively and will print the file and the line
- where this file is mentioned. Note that we used just the filename
- (`lfs_administration.md`) and not the whole the relative path
- (`workflow/lfs/lfs_administration.md`).
+Things to note:
+- Since we also use inline documentation, except for the documentation itself,
+ the document might also be referenced in the views of GitLab (`app/`) which will
+ render when visiting `/help`, and sometimes in the testing suite (`spec/`).
+- The above `git grep` command will search recursively in the directory you run
+ it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
+ and will print the file and the line where this file is mentioned.
+ You may ask why the two greps. Since we use relative paths to link to
+ documentation, sometimes it might be useful to search a path deeper.
+- The `*.md` extension is not used when a document is linked to GitLab's
+ built-in help page, that's why we omit it in `git grep`.
## Configuration documentation for source and Omnibus installations
@@ -244,6 +304,12 @@ In this case:
Here is a list of must-have items. Use them in the exact order that appears
on this document. Further explanation is given below.
+- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods)
+ (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
+ for a good example):
+ - `desc` for the method summary (you can pass it a block for additional details)
+ - `params` for the method params (this acts as description **and** validation
+ of the params)
- Every method must have the REST API request. For example:
```
@@ -297,7 +363,7 @@ Below is a set of [cURL][] examples that you can use in the API documentation.
Get the details of a group:
```bash
-curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
```
#### cURL example with parameters passed in the URL
@@ -305,7 +371,7 @@ curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/
Create a new project under the authenticated user's namespace:
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
```
#### Post data using cURL's --data
@@ -315,7 +381,7 @@ cURL's `--data` option. The example below will create a new project `foo` under
the authenticated user's namespace.
```bash
-curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
```
#### Post data using JSON content
@@ -324,7 +390,7 @@ curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.
and double quotes.
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
```
#### Post data using form-data
@@ -333,7 +399,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which
properly handles data encoding:
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
```
The above example is run by and administrator and will add an SSH public key
@@ -347,7 +413,7 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
ASCII code.
```bash
-curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
```
Use `%2F` for slashes (`/`).
@@ -359,10 +425,13 @@ restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
`example.net`, you would do something like this:
```bash
-curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domains[]=*.example.com" -d "restricted_signup_domains[]=example.net" https://gitlab.example.com/api/v3/application/settings
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v3/application/settings
```
[cURL]: http://curl.haxx.se/ "cURL website"
[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
-[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
+[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
+[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
+[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 9d7fe7440d2..159d5ce286d 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -41,10 +41,10 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897
-## Don't use inline CoffeeScript in views
+## Don't use inline CoffeeScript/JavaScript in views
Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
-performance overhead.
+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._
@@ -52,6 +52,7 @@ in an initializer._
### Further reading
- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
+- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
## ID-based CSS selectors need to be a bit more specific
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index e2ca46504e7..b8fab3aaff7 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -11,7 +11,8 @@ migrations are written carefully, can be applied online and adhere to the style
Migrations should not require GitLab installations to be taken offline unless
_absolutely_ necessary. If a migration requires downtime this should be
clearly mentioned during the review process as well as being documented in the
-monthly release post.
+monthly release post. For more information see the "Downtime Tagging" section
+below.
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible
@@ -20,35 +21,34 @@ about the state of the database.
Please don't depend on GitLab specific code since it can change in future versions.
If needed copy-paste GitLab code into the migration to make it forward compatible.
-## Comments in the migration
+## Downtime Tagging
-Each migration you write needs to have the two following pieces of information
-as comments.
+Every migration must specify if it requires downtime or not, and if it should
+require downtime it must also specify a reason for this. To do so, add the
+following two constants to the migration class' body:
-### Online, Offline, errors?
+* `DOWNTIME`: a boolean that when set to `true` indicates the migration requires
+ downtime.
+* `DOWNTIME_REASON`: a String containing the reason for the migration requiring
+ downtime. This constant **must** be set when `DOWNTIME` is set to `true`.
-First, you need to provide information on whether the migration can be applied:
+For example:
-1. online without errors (works on previous version and new one)
-2. online with errors on old instances after migrating
-3. online with errors on new instances while migrating
-4. offline (needs to happen without app servers to prevent db corruption)
-
-For example:
-
-```
-# Migration type: online without errors (works on previous version and new one)
+```ruby
class MyMigration < ActiveRecord::Migration
-...
-```
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because ...'
-It is always preferable to have a migration run online. If you expect the migration
-to take particularly long (for instance, if it loops through all notes),
-this is valuable information to add.
+ def change
+ ...
+ end
+end
+```
-If you don't provide the information it means that a migration is safe to run online.
+It is an error (that is, CI will fail) if the `DOWNTIME` constant is missing
+from a migration class.
-### Reversibility
+## Reversibility
Your migration should be reversible. This is very important, as it should
be possible to downgrade in case of a vulnerability or bugs.
@@ -100,7 +100,7 @@ value of `10` you'd write the following:
class MyMigration < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
-
+
def up
add_column_with_default(:projects, :foo, :integer, default: 10)
end
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
new file mode 100644
index 00000000000..e03adcaadea
--- /dev/null
+++ b/doc/development/newlines_styleguide.md
@@ -0,0 +1,102 @@
+# Newlines styleguide
+
+This style guide recommends best practices for newlines in Ruby code.
+
+## Rule: separate code with newlines only when it makes sense from logic perspectice
+
+```ruby
+# bad
+def method
+ issue = Issue.new
+
+ issue.save
+
+ render json: issue
+end
+```
+
+```ruby
+# good
+def method
+ issue = Issue.new
+ issue.save
+
+ render json: issue
+end
+```
+
+## Rule: separate code and block with newlines
+
+### Newline before block
+
+```ruby
+# bad
+def method
+ issue = Issue.new
+ if issue.save
+ render json: issue
+ end
+end
+```
+
+```ruby
+# good
+def method
+ issue = Issue.new
+
+ if issue.save
+ render json: issue
+ end
+end
+```
+
+## Newline after block
+
+```ruby
+# bad
+def method
+ if issue.save
+ issue.send_email
+ end
+ render json: issue
+end
+```
+
+```ruby
+# good
+def method
+ if issue.save
+ issue.send_email
+ end
+
+ render json: issue
+end
+```
+
+### Exception: no need for newline when code block starts or ends right inside another code block
+
+```ruby
+# bad
+def method
+
+ if issue
+
+ if issue.valid?
+ issue.save
+ end
+
+ end
+
+end
+```
+
+```ruby
+# good
+def method
+ if issue
+ if issue.valid?
+ issue.save
+ end
+ end
+end
+```
diff --git a/doc/development/performance.md b/doc/development/performance.md
index fb37b3a889c..7ff603e2c4a 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -15,8 +15,8 @@ The process of solving performance problems is roughly as follows:
3. Add your findings based on the measurement period (screenshots of graphs,
timings, etc) to the issue mentioned in step 1.
4. Solve the problem.
-5. Create a merge request, assign the "performance" label and ping the right
- people (e.g. [@yorickpeterse][yorickpeterse] and [@joshfng][joshfng]).
+5. Create a merge request, assign the "Performance" label and assign it to
+ [@yorickpeterse][yorickpeterse] for reviewing.
6. Once a change has been deployed make sure to _again_ measure for at least 24
hours to see if your changes have any impact on the production environment.
7. Repeat until you're done.
@@ -36,8 +36,8 @@ graphs/dashboards.
GitLab provides two built-in tools to aid the process of improving performance:
-* [Sherlock](doc/development/profiling.md#sherlock)
-* [GitLab Performance Monitoring](doc/monitoring/performance/monitoring.md)
+* [Sherlock](profiling.md#sherlock)
+* [GitLab Performance Monitoring](../monitoring/performance/monitoring.md)
GitLab employees can use GitLab.com's performance monitoring systems located at
<http://performance.gitlab.net>, this requires you to log in using your
@@ -254,5 +254,4 @@ referencing an object directly may even slow code down.
[#15607]: https://gitlab.com/gitlab-org/gitlab-ce/issues/15607
[yorickpeterse]: https://gitlab.com/u/yorickpeterse
-[joshfng]: https://gitlab.com/u/joshfng
[anti-pattern]: https://en.wikipedia.org/wiki/Anti-pattern
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 41685c7ee41..a7175f3f87e 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -14,11 +14,33 @@ Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests
-This runs all test suites present in GitLab.
+In order to run the test you can use the following commands:
+- `rake spinach` to run the spinach suite
+- `rake spec` to run the rspec suite
+- `rake teaspoon` to run the teaspoon test suite
+- `rake gitlab:test` to run all the tests
-```
-bundle exec rake test
-```
+Note: Both `rake spinach` and `rake spec` takes significant time to pass.
+Instead of running full test suite locally you can save a lot of time by running
+a single test or directory related to your changes. After you submit merge request
+CI will run full test suite for you. Green CI status in the merge request means
+full test suite is passed.
+
+Note: You can't run `rspec .` since this will try to run all the `_spec.rb`
+files it can find, also the ones in `/tmp`
+
+To run a single test file you can use:
+
+- `bundle exec rspec spec/controllers/commit_controller_spec.rb` for a rspec test
+- `bundle exec spinach features/project/issues/milestones.feature` for a spinach test
+
+To run several tests inside one directory:
+
+- `bundle exec rspec spec/requests/api/` for the rspec tests if you want to test API only
+- `bundle exec spinach features/profile/` for the spinach tests if you want to test only profile pages
+
+If you want to use [Spring](https://github.com/rails/spring) set
+`ENABLE_SPRING=1` in your environment.
## Generate searchable docs for source code
@@ -53,3 +75,8 @@ Generating a sprite file containing all the Emoji can be done by running:
```
bundle exec rake gemojione:sprite
```
+
+If new emoji are added, the spritesheet may change size. To compensate for
+such changes, first generate the `emoji.png` spritesheet with the above Rake
+task, then check the dimensions of the new spritesheet and update the
+`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants accordingly.
diff --git a/doc/development/ui_guide.md b/doc/development/ui_guide.md
index ce0aaa2fd25..2d1d504202c 100644
--- a/doc/development/ui_guide.md
+++ b/doc/development/ui_guide.md
@@ -1,43 +1,48 @@
-# UI Guide for building GitLab
+# UI Guide for building GitLab
## 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
+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.
+All design files are stored in the [gitlab-design](https://gitlab.com/gitlab-org/gitlab-design)
+repository and maintained by GitLab UX designers.
## Navigation
-GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
-This menu will be visible regardless of what page you visit. The left sidebar also contains the GitLab logo
-and the current user's profile picture. The content section contains a header and the content itself.
-The header describes the current GitLab page and what navigation is
-available to user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example when user visits one of the
-project pages the header will contain a project name and navigation for that project. When the user visits a group page it will contain a group name and navigation related to this group.
+GitLab's layout contains 2 sections: the left sidebar and the content. The left sidebar contains a static navigation menu.
+This menu will be visible regardless of what page you visit.
+The content section contains a header and the content itself. The header describes the current GitLab page and what navigation is
+available to the user in this area. Depending on the area (project, group, profile setting) the header name and navigation may change. For example, when the user visits one of the
+project pages the header will contain the project's name and navigation for that project. When the user visits a group page it will contain the group's name and navigation related to this group.
+
+You can see a visual representation of the navigation in GitLab in the GitLab Product Map, which is located in the [Design Repository](gitlab-map-graffle)
+along with [PDF](gitlab-map-pdf) and [PNG](gitlab-map-png) exports.
+
### Adding new tab to header navigation
-We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each
-tab should represent separate functionality. Everything related to the issue
-tracker should be under the 'Issues' tab while everything related to the wiki should
+We try to keep the amount of tabs in the header navigation between 5 and 10 so that it fits on a typical laptop screen. We also try not to confuse the user with too many options. Ideally each
+tab should represent separate functionality. Everything related to the issue
+tracker should be under the 'Issues' tab while everything related to the wiki should
be under 'Wiki' tab and so on and so forth.
+When adding a new tab to the header don't use more than 2 words for text in the link.
+We want to keep links short and easy to remember and fit all of them in the small screen.
-## Mobile screen size
+## Mobile screen size
-We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide
-part of the UI for smaller resolutions in favor of a better user experience.
+We want GitLab to work well on small mobile screens as well. Size limitations make it is impossible to fit everything on a mobile screen. In this case it is OK to hide
+part of the UI for smaller resolutions in favor of a better user experience.
However core functionality like browsing files, creating issues, writing comments, should
be available on all resolutions.
## Icons
-* `trash` icon for button or link that does destructive action like removing
+* `trash` icon for button or link that does destructive action like removing
information from database or file system
* `x` icon for closing/hiding UI element. For example close modal window
* `pencil` icon for edit button or link
@@ -45,13 +50,58 @@ information from database or file system
* `rss` for rss/atom feed
* `plus` for link or dropdown that lead to page where you create new object (For example new issue page)
+### 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>
+```
+
## Buttons
* Button should contain icon or text. Exceptions should be approved by UX designer.
* Use red button for destructive actions (not revertable). For example removing issue.
-* Use green or blue button for primary action. Primary button should be only one.
-Do not use both green and blue button in one form.
-* For all other cases use default white button.
-* Text button should have only first word capitalized. So should be "Create issue" instead of "Create Issue"
+* Use green or blue button for primary action. Primary button should be only one.
+Do not use both green and blue button in one form.
+* For all other cases use default white button.
+* Text button should have only first word capitalized. So should be "Create issue" instead of "Create Issue"
+
+## Counts
+
+* Always use the [`number_with_delimiter`][number_with_delimiter] helper to
+ display counts in the UI.
+[number_with_delimiter]: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#method-i-number_with_delimiter
+[gitlab-map-graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/master/production/resources/gitlab-map.graffle
+[gitlab-map-pdf]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.pdf
+[gitlab-map-png]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
new file mode 100644
index 00000000000..2574c2c0472
--- /dev/null
+++ b/doc/development/what_requires_downtime.md
@@ -0,0 +1,161 @@
+# What requires downtime?
+
+When working with a database certain operations can be performed without taking
+GitLab offline, others do require a downtime period. This guide describes
+various operations and their impact.
+
+## Adding Columns
+
+On PostgreSQL you can safely add a new column to an existing table as long as it
+does **not** have a default value. For example, this query would not require
+downtime:
+
+```sql
+ALTER TABLE projects ADD COLUMN random_value int;
+```
+
+Add a column _with_ a default however does require downtime. For example,
+consider this query:
+
+```sql
+ALTER TABLE projects ADD COLUMN random_value int DEFAULT 42;
+```
+
+This requires updating every single row in the `projects` table so that
+`random_value` is set to `42` by default. This requires updating all rows and
+indexes in a table. This in turn acquires enough locks on the table for it to
+effectively block any other queries.
+
+As of MySQL 5.6 adding a column to a table is still quite an expensive
+operation, even when using `ALGORITHM=INPLACE` and `LOCK=NONE`. This means
+downtime _may_ be required when modifying large tables as otherwise the
+operation could potentially take hours to complete.
+
+Adding a column with a default value _can_ be done without requiring downtime
+when using the migration helper method
+`Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works
+similar to `add_column` except it updates existing rows in batches without
+blocking access to the table being modified. See ["Adding Columns With Default
+Values"](migration_style_guide.html#adding-columns-with-default-values) for more
+information on how to use this method.
+
+## Dropping Columns
+
+On PostgreSQL you can safely remove an existing column without the need for
+downtime. When you drop a column in PostgreSQL it's not immediately removed,
+instead it is simply disabled. The data is removed on the next vacuum run.
+
+On MySQL this operation requires downtime.
+
+While database wise dropping a column may be fine on PostgreSQL this operation
+still requires downtime because the application code may still be using the
+column that was removed. For example, consider the following migration:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ def change
+ remove_column :projects, :dummy
+ end
+end
+```
+
+Now imagine that the GitLab instance is running and actively uses the `dummy`
+column. If we were to run the migration this would result in the GitLab instance
+producing errors whenever it tries to use the `dummy` column.
+
+As a result of the above downtime _is_ required when removing a column, even
+when using PostgreSQL.
+
+## Changing Column Constraints
+
+Generally changing column constraints requires checking all rows in the table to
+see if they meet the new constraint, unless a constraint is _removed_. For
+example, changing a column that previously allowed NULL values to not allow NULL
+values requires the database to verify all existing rows.
+
+The specific behaviour varies a bit between databases but in general the safest
+approach is to assume changing constraints requires downtime.
+
+## Changing Column Types
+
+This operation requires downtime.
+
+## Adding Indexes
+
+Adding indexes is an expensive process that blocks INSERT and UPDATE queries for
+the duration. When using PostgreSQL one can work arounds this by using the
+`CONCURRENTLY` option:
+
+```sql
+CREATE INDEX CONCURRENTLY index_name ON projects (column_name);
+```
+
+Migrations can take advantage of this by using the method
+`add_concurrent_index`. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ def change
+ add_concurrent_index :projects, :column_name
+ end
+end
+```
+
+When running this on PostgreSQL the `CONCURRENTLY` option mentioned above is
+used. On MySQL this method produces a regular `CREATE INDEX` query.
+
+MySQL doesn't really have a workaround for this. Supposedly it _can_ create
+indexes without the need for downtime but only for variable width columns. The
+details on this are a bit sketchy. Since it's better to be safe than sorry one
+should assume that adding indexes requires downtime on MySQL.
+
+## Dropping Indexes
+
+Dropping an index does not require downtime on both PostgreSQL and MySQL.
+
+## Adding Tables
+
+This operation is safe as there's no code using the table just yet.
+
+## Dropping Tables
+
+This operation requires downtime as application code may still be using the
+table.
+
+## Adding Foreign Keys
+
+Adding foreign keys acquires an exclusive lock on both the source and target
+tables in PostgreSQL. This requires downtime as otherwise the entire application
+grinds to a halt for the duration of the operation.
+
+On MySQL this operation also requires downtime _unless_ foreign key checks are
+disabled. Because this means checks aren't enforced this is not ideal, as such
+one should assume MySQL also requires downtime.
+
+## Removing Foreign Keys
+
+This operation should not require downtime on both PostgreSQL and MySQL.
+
+## Updating Data
+
+Updating data should generally be safe. The exception to this is data that's
+being migrated from one version to another while the application still produces
+data in the old version.
+
+For example, imagine the application writes the string `'dog'` to a column but
+it really is meant to write `'cat'` instead. One might think that the following
+migration is all that is needed to solve this problem:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+ def up
+ execute("UPDATE some_table SET column = 'cat' WHERE column = 'dog';")
+ end
+end
+```
+
+Unfortunately this is not enough. Because the application is still running and
+using the old value this may result in the table still containing rows where
+`column` is set to `dog`, even after the migration finished.
+
+In these cases downtime _is_ required, even for rarely updated tables.
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 89ce8bcc3e8..b61f436c1a4 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -120,3 +120,11 @@ You need to be in the created branch.
git checkout NAME-OF-BRANCH
git merge master
```
+
+### Merge master branch with created branch
+You need to be in the master branch.
+```
+git checkout master
+git merge NAME-OF-BRANCH
+```
+
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 19d083d580d..d4b89fa8345 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -89,7 +89,7 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl -O --progress https://www.kernel.org/pub/software/scm/git/git-2.7.4.tar.gz
+ 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/
./configure
@@ -108,8 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-_**Note:** The current supported Ruby version is 2.1.x. Ruby 2.2 and 2.3 are
-currently not supported._
+_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
@@ -124,9 +123,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.8.tar.gz
- echo 'c7e50159357afd87b13dc5eaf4ac486a70011149 ruby-2.1.8.tar.gz' | shasum -c - && tar xzf ruby-2.1.8.tar.gz
- cd ruby-2.1.8
+ 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
./configure --disable-install-rdoc
make
sudo make install
@@ -143,7 +142,7 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
- curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
+ curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
@@ -269,9 +268,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-10-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-11-stable gitlab
-**Note:** You can change `8-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-11-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -398,7 +397,7 @@ If you are not using Linux you may have to run `gmake` instead of
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 v0.7.7
+ sudo -u git -H git checkout v0.7.11
sudo -u git -H make
### Initialize Database and Activate Advanced Features
@@ -588,15 +587,17 @@ for the changes to take effect.
### Custom Redis Connection
-If you'd like Resque to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
+If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file.
# example
- production: redis://redis.example.tld:6379
+ production:
+ url: redis://redis.example.tld:6379
If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file.
# example
- production: unix:/path/to/redis/socket
+ production:
+ url: unix:/path/to/redis/socket
### Custom SSH Connection
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index a65ac8a5f79..571f1a38358 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions
-GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 or 2.3.
+GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13.
You will have to use the standard MRI implementation of Ruby.
We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
diff --git a/doc/integration/README.md b/doc/integration/README.md
index fd330dd7a7d..c2fd299db07 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -11,11 +11,11 @@ See the documentation below for details on how to configure these services.
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
-- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
+- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index 5cc09bd536d..a6436b5f926 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -1,9 +1,14 @@
# Akismet
+> *Note:* Before 8.11 only issues submitted via the API and for non-project
+members were submitted to Akismet.
+
GitLab leverages [Akismet](http://akismet.com) to protect against spam. Currently
-GitLab uses Akismet to prevent users who are not members of a project from
-creating spam via the GitLab API. Detected spam will be rejected, and
-an entry in the "Spam Log" section in the Admin page will be created.
+GitLab uses Akismet to prevent the creation of spam issues on public projects. Issues
+created via the WebUI or the API can be submitted to Akismet for review.
+
+Detected spam will be rejected, and an entry in the "Spam Log" section in the
+Admin page will be created.
Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that
adding a user to a project will disable the Akismet check and prevent this
@@ -17,14 +22,37 @@ To use Akismet:
2. Sign-in or create a new account.
-3. Click on "Show" to reveal the API key.
+3. Click on **Show** to reveal the API key.
4. Go to Applications Settings on Admin Area (`admin/application_settings`)
-5. Check the `Enable Akismet` checkbox
+5. Check the **Enable Akismet** checkbox
6. Fill in the API key from step 3.
7. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png)
+
+
+## Training
+
+> *Note:* Training the Akismet filter is only available in 8.11 and above.
+
+As a way to better recognize between spam and ham, you can train the Akismet
+filter whenever there is a false positive or false negative.
+
+When an entry is recognized as spam, it is rejected and added to the Spam Logs.
+From here you can review if they are really spam. If one of them is not really
+spam, you can use the **Submit as ham** button to tell Akismet that it falsely
+recognized an entry as spam.
+
+![Screenshot of Spam Logs](img/spam_log.png)
+
+If an entry that is actually spam was not recognized as such, you will be able
+to also submit this to Akismet. The **Submit as spam** button will only appear
+to admin users.
+
+![Screenshot of Issue](img/submit_issue.png)
+
+Training Akismet will help it to recognize spam more accurately in the future.
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 63432b04432..2eb6266ebe7 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -14,7 +14,7 @@ Bitbucket will generate an application ID and secret key for you to use.
1. Select "Add consumer".
1. Provide the required details.
- - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
+ - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Application description: Fill this in if you wish.
- URL: The URL to your GitLab installation. 'https://gitlab.company.com'
1. Select "Save".
diff --git a/doc/integration/github.md b/doc/integration/github.md
index 340c8a55fb3..8a01afd1177 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -16,7 +16,7 @@ GitHub will generate an application ID and secret key for you to use.
1. Select "Register new application".
1. Provide the required details.
- - Application name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
+ - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Homepage URL: The URL to your GitLab installation. 'https://gitlab.company.com'
- Application description: Fill this in if you wish.
- Authorization callback URL is 'http(s)://${YOUR_DOMAIN}'
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index b215cc7c609..6d8f3912ede 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -14,7 +14,7 @@ GitLab.com will generate an application ID and secret key for you to use.
1. Select "New application".
1. Provide the required details.
- - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or something else descriptive.
+ - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- Redirect URI:
```
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
new file mode 100644
index 00000000000..8d574448690
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..5c7896a7eec
--- /dev/null
+++ b/doc/integration/img/submit_issue.png
Binary files differ
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 5f8bb57365c..0c53584d201 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -28,7 +28,8 @@ GitLab supports two ways of adding a new OAuth2 application to an instance. You
can either add an application as a regular user or add it in the admin area.
What this means is that GitLab can actually have instance-wide and a user-wide
applications. There is no difference between them except for the different
-permission levels they are set (user/admin).
+permission levels they are set (user/admin). The default callback URL is
+`http://your-gitlab.example.com/users/auth/gitlab/callback`
## Adding an application through the profile
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 8a7205caaa4..f3b2a288776 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -138,7 +138,7 @@ This setting is only available on GitLab 8.7 and above.
SAML login includes support for external groups. You can define in the SAML
settings which groups, to which your users belong in your IdP, you wish to be
-marked as [external](../permissions/permissions.md).
+marked as [external](../user/permissions.md).
### Requirements
@@ -306,4 +306,4 @@ For this you need take the following into account:
validators are optional
Make sure that one of the above described scenarios is valid, or the requests will
-fail with one of the mentioned errors. \ No newline at end of file
+fail with one of the mentioned errors.
diff --git a/doc/integration/slack.md b/doc/integration/slack.md
index f6ba80f46d5..8cd151fbf95 100644
--- a/doc/integration/slack.md
+++ b/doc/integration/slack.md
@@ -1,41 +1 @@
-# Slack integration
-
-## On Slack
-
-To enable Slack integration you must create an Incoming WebHooks integration on Slack:
-
-1. [Sign in to Slack](https://slack.com/signin)
-
-1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
-
-1. Choose the channel name you want to send notifications to.
-
-1. Click **Add Incoming WebHooks Integration**
- - Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**.
-
-1. Copy the **Webhook URL**, we'll need this later for GitLab.
-
-
-## On GitLab
-
-After Slack is ready we need to setup GitLab. Here are the steps to achieve this.
-
-1. Sign in to GitLab
-
-1. Pick the repository you want.
-
-1. Navigate to Settings -> Services -> Slack
-
-1. Pick the triggers you want to activate
-
-1. Fill in your Slack details
- - Webhook: Paste the Webhook URL from the step above
- - Username: Fill this in if you want to change the username of the bot
- - Channel: Fill this in if you want to change the channel where the messages will be posted
- - Mark it as active
-
-1. Save your settings
-
-Have fun :)
-
-*P.S. You can set "branch,pushed,Compare changes" as highlight words on your Slack profile settings, so that you can be aware of new commits when somebody pushes them.*
+This document was moved to [project_services/slack.md](../project_services/slack.md).
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 4769f26b259..abbea09f22f 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -7,7 +7,7 @@ To enable the Twitter OmniAuth provider you must register your application with
1. Select "Create new app"
1. Fill in the application details.
- - Name: This can be anything. Consider something like "\<Organization\>'s GitLab" or "\<Your Name\>'s GitLab" or
+ - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or
something else descriptive.
- Description: Create a description.
- Website: The URL to your GitLab installation. 'https://gitlab.example.com'
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 7b94506c297..7f08188bd65 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -6,13 +6,17 @@ You accept and agree to the following terms and conditions for Your present and
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with GitLab B.V.. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
- "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
+ "Contribution" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, that is submitted by You to GitLab B.V. for inclusion in, or documentation of, any of the products owned or managed by GitLab B.V. (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to GitLab B.V. or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, GitLab B.V. for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution."
-2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
+2. Grant of Copyright License.
-3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
+Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
-4. You represent that You are legally entitled to grant the above license. You represent further that each employee of the Corporation designated on Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation.
+3. Grant of Patent License.
+
+Subject to the terms and conditions of this Agreement, You hereby grant to GitLab B.V. and to recipients of software distributed by GitLab B.V. a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
+
+4. You represent that You are legally entitled to grant the above license. You represent further that each of Your employees is authorized to submit Contributions on Your behalf, but excluding employees that are designated in writing by You as "Not authorized to submit Contributions on behalf of [name of Your corporation here]." Such designations of exclusion for unauthorized employees are to be submitted via email to legal@gitlab.com.
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
@@ -20,6 +24,6 @@ You accept and agree to the following terms and conditions for Your present and
7. Should You wish to submit work that is not Your original creation, You may submit it to GitLab B.V. separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
-8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V..
+8. It is Your responsibility to notify GitLab.com when any change is required to the list of designated employees excluded from submitting Contributions on Your behalf per Section 4. Such notification should be sent via email to legal@gitlab.com.
This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index fb2dd582754..4ac81ab3ee7 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -1,647 +1 @@
-# 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)
-
-**[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)
-
-**[References](#references)**
-
-## GitLab Flavored Markdown (GFM)
-
-_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
-
-GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
-
-You can use GFM in
-
-- comments
-- issues
-- merge requests
-- milestones
-- wiki pages
-
-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
-
-GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
-
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
-Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
-
- Roses are red [followed by two or more spaces]
- Violets are blue
-
- Sugar is sweet
-
-Roses are red
-Violets are blue
-
-Sugar is sweet
-
-## Multiple underscores in words
-
-It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words.
-
- perform_complicated_task
- do_this_and_do_that_and_another_thing
-
-perform_complicated_task
-do_this_and_do_that_and_another_thing
-
-## URL auto-linking
-
-GFM will autolink almost any URL you copy and paste into your text.
-
- * https://www.google.com
- * https://google.com/
- * ftp://ftp.us.debian.org/debian/
- * smb://foo/bar/baz
- * irc://irc.freenode.net/gitlab
- * http://localhost:3000
-
-* https://www.google.com
-* https://google.com/
-* ftp://ftp.us.debian.org/debian/
-* smb://foo/bar/baz
-* irc://irc.freenode.net/gitlab
-* http://localhost:3000
-
-## Multiline Blockquote
-
-On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
-GFM supports multiline blockquotes fenced by <code>>>></code>.
-
-```no-highlight
->>>
-If you paste a message from somewhere else
-
-that
-
-spans
-
-multiple lines,
-
-you can quote that without having to manually prepend `>` to every line!
->>>
-```
-
->>>
-If you paste a message from somewhere else
-
-that
-
-spans
-
-multiple lines,
-
-you can quote that without having to manually prepend `>` to every line!
->>>
-
-## Code and Syntax Highlighting
-
-_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
-list of supported languages visit the Rouge website._
-
-Blocks of code are either fenced by lines with three back-ticks <code>```</code>, or are indented with four spaces. Only the fenced code blocks support syntax highlighting.
-
-```no-highlight
-Inline `code` has `back-ticks around` it.
-```
-
-Inline `code` has `back-ticks around` it.
-
-Example:
-
- ```javascript
- var s = "JavaScript syntax highlighting";
- alert(s);
- ```
-
- ```python
- def function():
- #indenting works just fine in the fenced code block
- s = "Python syntax highlighting"
- print s
- ```
-
- ```ruby
- require 'redcarpet'
- markdown = Redcarpet.new("Hello World!")
- puts markdown.to_html
- ```
-
- ```
- No language indicated, so no syntax highlighting.
- s = "There is no highlighting for this."
- But let's throw in a <b>tag</b>.
- ```
-
-becomes:
-
-```javascript
-var s = "JavaScript syntax highlighting";
-alert(s);
-```
-
-```python
-def function():
- #indenting works just fine in the fenced code block
- s = "Python syntax highlighting"
- print s
-```
-
-```ruby
-require 'redcarpet'
-markdown = Redcarpet.new("Hello World!")
-puts markdown.to_html
-```
-
-```
-No language indicated, so no syntax highlighting.
-s = "There is no highlighting for this."
-But let's throw in a <b>tag</b>.
-```
-
-## Inline Diff
-
-With inline diffs tags you can display {+ additions +} or [- deletions -].
-
-The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
-
-However the wrapping tags cannot be mixed as such:
-
-- {+ additions +]
-- [+ additions +}
-- {- deletions -]
-- [- deletions -}
-
-## Emoji
-
- Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
-
- :zap: You can use emoji anywhere GFM is supported. :v:
-
- You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-
- If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
-
- Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-
-Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
-
-:zap: You can use emoji anywhere GFM is supported. :v:
-
-You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-
-If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
-
-Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-
-## Special GitLab References
-
-GFM recognizes special references.
-
-You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
-
-GFM will turn that reference into a link so you can navigate between them easily.
-
-GFM will recognize the following:
-
-| input | references |
-|:-----------------------|:--------------------------- |
-| `@user_name` | specific user |
-| `@group_name` | specific group |
-| `@all` | entire team |
-| `#123` | issue |
-| `!123` | merge request |
-| `$123` | snippet |
-| `~123` | label by ID |
-| `~bug` | one-word label by name |
-| `~"feature request"` | multi-word label by name |
-| `%123` | milestone by ID |
-| `%v1.23` | one-word milestone by name |
-| `%"release candidate"` | multi-word milestone by name |
-| `9ba12248` | specific commit |
-| `9ba12248...b19a04f5` | commit range comparison |
-| `[README](doc/README)` | repository file references |
-
-GFM also recognizes certain cross-project references:
-
-| input | references |
-|:----------------------------------------|:------------------------|
-| `namespace/project#123` | issue |
-| `namespace/project!123` | merge request |
-| `namespace/project%123` | milestone |
-| `namespace/project$123` | snippet |
-| `namespace/project@9ba12248` | specific commit |
-| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
-| `namespace/project~"Some label"` | issues with given label |
-
-## Task Lists
-
-You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
-
-```no-highlight
-- [x] Completed task
-- [ ] Incomplete task
- - [ ] Sub-task 1
- - [x] Sub-task 2
- - [ ] Sub-task 3
-```
-
-- [x] Completed task
-- [ ] Incomplete task
- - [ ] Sub-task 1
- - [x] Sub-task 2
- - [ ] Sub-task 3
-
-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.
-
-# Standard Markdown
-
-## Headers
-
-```no-highlight
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
-
-Alternatively, for H1 and H2, an underline-ish style:
-
-Alt-H1
-======
-
-Alt-H2
-------
-```
-
-# H1
-## H2
-### H3
-#### H4
-##### H5
-###### H6
-
-Alternatively, for H1 and H2, an underline-ish style:
-
-Alt-H1
-======
-
-Alt-H2
-------
-
-### Header IDs and links
-
-All Markdown-rendered headers automatically get IDs, except in comments.
-
-On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
-
-The IDs are generated from the content of the header according to the following rules:
-
-1. All text is converted to lowercase
-1. All non-word text (e.g., punctuation, HTML) is removed
-1. All spaces are converted to hyphens
-1. Two or more hyphens in a row are converted to one
-1. If a header with the same ID has already been generated, a unique
- incrementing number is appended, starting at 1.
-
-For example:
-
-```
-# This header has spaces in it
-## This header has a :thumbsup: in it
-# This header has Unicode in it: 한글
-## This header has spaces in it
-### This header has spaces in it
-```
-
-Would generate the following link IDs:
-
-1. `this-header-has-spaces-in-it`
-1. `this-header-has-a-in-it`
-1. `this-header-has-unicode-in-it-한글`
-1. `this-header-has-spaces-in-it`
-1. `this-header-has-spaces-in-it-1`
-
-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
-
-```no-highlight
-Emphasis, aka italics, with *asterisks* or _underscores_.
-
-Strong emphasis, aka bold, with **asterisks** or __underscores__.
-
-Combined emphasis with **asterisks and _underscores_**.
-
-Strikethrough uses two tildes. ~~Scratch this.~~
-```
-
-Emphasis, aka italics, with *asterisks* or _underscores_.
-
-Strong emphasis, aka bold, with **asterisks** or __underscores__.
-
-Combined emphasis with **asterisks and _underscores_**.
-
-Strikethrough uses two tildes. ~~Scratch this.~~
-
-## Lists
-
-```no-highlight
-1. First ordered list item
-2. Another item
- * Unordered sub-list.
-1. Actual numbers don't matter, just that it's a number
- 1. Ordered sub-list
-4. And another item.
-
-* Unordered list can use asterisks
-- Or minuses
-+ Or pluses
-```
-
-1. First ordered list item
-2. Another item
- * Unordered sub-list.
-1. Actual numbers don't matter, just that it's a number
- 1. Ordered sub-list
-4. And another item.
-
-* Unordered list can use asterisks
-- Or minuses
-+ Or pluses
-
-If a list item contains multiple paragraphs,
-each subsequent paragraph should be indented with four spaces.
-
-```no-highlight
-1. First ordered list item
-
- Second paragraph of first item.
-2. Another item
-```
-
-1. First ordered list item
-
- Second paragraph of first item.
-2. Another item
-
-If the second paragraph isn't indented with four spaces,
-the second list item will be incorrectly labeled as `1`.
-
-```no-highlight
-1. First ordered list item
-
- Second paragraph of first item.
-2. Another item
-```
-
-1. First ordered list item
-
- Second paragraph of first item.
-2. Another item
-
-## Links
-
-There are two ways to create links, inline-style and reference-style.
-
- [I'm an inline-style link](https://www.google.com)
-
- [I'm a reference-style link][Arbitrary case-insensitive reference text]
-
- [I'm a relative reference to a repository file](LICENSE)
-
- [You can use numbers for reference-style link definitions][1]
-
- Or leave it empty and use the [link text itself][]
-
- Some text to show that the reference links can follow later.
-
- [arbitrary case-insensitive reference text]: https://www.mozilla.org
- [1]: http://slashdot.org
- [link text itself]: https://www.reddit.com
-
-[I'm an inline-style link](https://www.google.com)
-
-[I'm a reference-style link][Arbitrary case-insensitive reference text]
-
-[I'm a relative reference to a repository file](LICENSE)[^1]
-
-[You can use numbers for reference-style link definitions][1]
-
-Or leave it empty and use the [link text itself][]
-
-Some text to show that the reference links can follow later.
-
-[arbitrary case-insensitive reference text]: https://www.mozilla.org
-[1]: http://slashdot.org
-[link text itself]: https://www.reddit.com
-
-**Note**
-
-Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
-
-`[I'm a reference-style link](style)`
-
-will point the link to `wikis/style` when the link is inside of a wiki markdown file.
-
-## Images
-
- Here's our logo (hover to see the title text):
-
- Inline-style:
- ![alt text](img/logo.png)
-
- Reference-style:
- ![alt text1][logo]
-
- [logo]: img/logo.png
-
-Here's our logo:
-
-Inline-style:
-
-![alt text](img/logo.png)
-
-Reference-style:
-
-![alt text][logo]
-
-[logo]: img/logo.png
-
-## Blockquotes
-
-```no-highlight
-> Blockquotes are very handy in email to emulate reply text.
-> This line is part of the same quote.
-
-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.
-```
-
-> Blockquotes are very handy in email to emulate reply text.
-> This line is part of the same quote.
-
-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
-
-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.
-
-```no-highlight
-<dl>
- <dt>Definition list</dt>
- <dd>Is something people use sometimes.</dd>
-
- <dt>Markdown in HTML</dt>
- <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
-</dl>
-```
-
-<dl>
- <dt>Definition list</dt>
- <dd>Is something people use sometimes.</dd>
-
- <dt>Markdown in HTML</dt>
- <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
-</dl>
-
-## Horizontal Rule
-
-```
-Three or more...
-
----
-
-Hyphens
-
-***
-
-Asterisks
-
-___
-
-Underscores
-```
-
-Three or more...
-
----
-
-Hyphens
-
-***
-
-Asterisks
-
-___
-
-Underscores
-
-## 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.
-
-Here are some things to try out:
-
-```
-Here's a line for us to start with.
-
-This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
-
-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 on its own line, because the previous line ends with two
-spaces.
-```
-
-Here's a line for us to start with.
-
-This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
-
-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 on its own line, because the previous line ends with two
-spaces.
-
-## Tables
-
-Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
-
-```
-| header 1 | header 2 |
-| -------- | -------- |
-| cell 1 | cell 2 |
-| cell 3 | cell 4 |
-```
-
-Code above produces next output:
-
-| header 1 | header 2 |
-| -------- | -------- |
-| cell 1 | cell 2 |
-| cell 3 | cell 4 |
-
-**Note**
-
-The row of dashes between the table header and body must have at least three dashes in each column.
-
-By including colons in the header row, you can align the text within that column:
-
-```
-| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
-| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
-| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
-| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
-```
-
-| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
-| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
-| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
-| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
-
-## 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.
-
-[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
+This document was moved to [user/markdown.md](../user/markdown.md).
diff --git a/doc/monitoring/health_check.md b/doc/monitoring/health_check.md
index 0d17799372f..eac57bc3de4 100644
--- a/doc/monitoring/health_check.md
+++ b/doc/monitoring/health_check.md
@@ -1,6 +1,6 @@
# Health Check
->**Note:** This feature was [introduced][ce-3888] in GitLab 8.8.
+> [Introduced][ce-3888] in GitLab 8.8.
GitLab provides a health check endpoint for uptime monitoring on the `health_check` web
endpoint. The health check reports on the overall system status based on the status of
@@ -24,7 +24,7 @@ https://gitlab.example.com/health_check.json?token=ACCESS_TOKEN
or as an HTTP header:
```bash
-curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
```
## Using the Endpoint
@@ -45,7 +45,7 @@ You can also ask for the status of specific services:
For example, the JSON output of the following health check:
```bash
-curl -H "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
+curl --header "TOKEN: ACCESS_TOKEN" https://gitlab.example.com/health_check.json
```
would be like:
diff --git a/doc/monitoring/performance/influxdb_schema.md b/doc/monitoring/performance/influxdb_schema.md
index 41861860b6d..eff0e29f58d 100644
--- a/doc/monitoring/performance/influxdb_schema.md
+++ b/doc/monitoring/performance/influxdb_schema.md
@@ -9,6 +9,7 @@ The following measurements are currently stored in InfluxDB:
- `PROCESS_object_counts`
- `PROCESS_transactions`
- `PROCESS_views`
+- `events`
Here, `PROCESS` is replaced with either `rails` or `sidekiq` depending on the
process type. In all series, any form of duration is stored in milliseconds.
@@ -78,6 +79,14 @@ following value fields are available:
The `action` tag contains the action name of the transaction that rendered the
view.
+## events
+
+This measurement is used to store generic events such as the number of Git
+pushes, Emails sent, etc. Each point in this measurement has a single value
+field called `count`. The value of this field is simply set to `1`. Each point
+also has at least one tag: `event`. This tag's value is set to the event name.
+Depending on the event type additional tags may be available as well.
+
---
Read more on:
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 44f3f6d3b12..78d67aeec78 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -1,104 +1,3 @@
# Permissions
-Users have different abilities depending on the access level they have in a particular group or project.
-
-If a user is both in a project group and in the project itself, the highest permission level is used.
-
-If a user is a GitLab administrator they receive all permissions.
-
-On public and internal projects the Guest role is not enforced.
-All users will be able to create issues, leave comments, and pull or download the project code.
-
-To add or import a user, you can follow the [project users and members
-documentation](../workflow/add-user/add-user.md).
-
-## Project
-
-| Action | Guest | Reporter | Developer | Master | Owner |
-|---------------------------------------|---------|------------|-------------|----------|--------|
-| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
-| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| Pull project code | | ✓ | ✓ | ✓ | ✓ |
-| Download project | | ✓ | ✓ | ✓ | ✓ |
-| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
-| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
-| Manage labels | | ✓ | ✓ | ✓ | ✓ |
-| See a commit status | | ✓ | ✓ | ✓ | ✓ |
-| See a container registry | | ✓ | ✓ | ✓ | ✓ |
-| See environments | | ✓ | ✓ | ✓ | ✓ |
-| Manage merge requests | | | ✓ | ✓ | ✓ |
-| Create new merge request | | | ✓ | ✓ | ✓ |
-| Create new branches | | | ✓ | ✓ | ✓ |
-| Push to non-protected branches | | | ✓ | ✓ | ✓ |
-| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
-| Remove non-protected branches | | | ✓ | ✓ | ✓ |
-| Add tags | | | ✓ | ✓ | ✓ |
-| Write a wiki | | | ✓ | ✓ | ✓ |
-| Cancel and retry builds | | | ✓ | ✓ | ✓ |
-| 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 | | | | ✓ | ✓ |
-| Enable/disable branch protection | | | | ✓ | ✓ |
-| Turn on/off prot. branch push for devs| | | | ✓ | ✓ |
-| Rewrite/remove git tags | | | | ✓ | ✓ |
-| Edit project | | | | ✓ | ✓ |
-| Add deploy keys to project | | | | ✓ | ✓ |
-| Configure project hooks | | | | ✓ | ✓ |
-| Manage runners | | | | ✓ | ✓ |
-| Manage build triggers | | | | ✓ | ✓ |
-| Manage variables | | | | ✓ | ✓ |
-| Delete environments | | | | ✓ | ✓ |
-| Switch visibility level | | | | | ✓ |
-| Transfer project to another namespace | | | | | ✓ |
-| Remove project | | | | | ✓ |
-| Force push to protected branches [^2] | | | | | |
-| Remove protected branches [^2] | | | | | |
-
-[^1]: If **Allow guest to access builds** is enabled in CI settings
-[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
-
-## Group
-
-In order for a group to appear as public and be browsable, it must contain at
-least one public project.
-
-Any user can remove themselves from a group, unless they are the last Owner of the group.
-
-| Action | Guest | Reporter | Developer | Master | Owner |
-|-------------------------|-------|----------|-----------|--------|-------|
-| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
-| Edit group | | | | | ✓ |
-| Create project in group | | | | ✓ | ✓ |
-| Manage group members | | | | | ✓ |
-| Remove group | | | | | ✓ |
-
-## External Users
-
-In cases where it is desired that a user has access only to some internal or
-private projects, there is the option of creating **External Users**. This
-feature may be useful when for example a contractor is working on a given
-project and should only have access to that project.
-
-External users can only access projects to which they are explicitly granted
-access, thus hiding all other internal or private ones from them. Access can be
-granted by adding the user as member to the project or group.
-
-They will, like usual users, receive a role in the project or group with all
-the abilities that are mentioned in the table above. They cannot however create
-groups or projects, and they have the same access as logged out users in all
-other cases.
-
-An administrator can flag a user as external [through the API](../api/users.md)
-or by checking the checkbox on the admin panel. As an administrator, navigate
-to **Admin > Users** to create a new user or edit an existing one. There, you
-will find the option to flag the user as external.
-
-By default new users are not set as external users. This behavior can be changed
-by an administrator under **Admin > Application Settings**. \ No newline at end of file
+This document was moved to [user/permissions.md](../user/permissions.md).
diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png
new file mode 100644
index 00000000000..b8de8a56db7
--- /dev/null
+++ b/doc/project_services/img/slack_configuration.png
Binary files differ
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index e15d5db3253..4442b7c1742 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -45,7 +45,7 @@ further configuration instructions and details. Contributions are welcome.
| 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 | A team communication tool for the 21st century |
+| [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
new file mode 100644
index 00000000000..3cfe77c9f85
--- /dev/null
+++ b/doc/project_services/slack.md
@@ -0,0 +1,50 @@
+# Slack Service
+
+## On Slack
+
+To enable Slack integration you must create an incoming webhook integration on
+Slack:
+
+1. [Sign in to Slack](https://slack.com/signin)
+1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/)
+1. Choose the channel name you want to send notifications to.
+1. Click **Add Incoming WebHooks Integration**
+1. Copy the **Webhook URL**, we'll need this later for GitLab.
+
+## On GitLab
+
+After you set up Slack, it's time to set up GitLab.
+
+Go to your project's **Settings > Services > Slack** and you will see a
+checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Slack channel you want to send that event message, with `#general`
+being the default. Enter your preferred channel **without** the hash sign (`#`).
+
+At the end, fill in your Slack details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
+| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+After you are all done, click **Save changes** for the changes to take effect.
+
+>**Note:**
+You can set "branch,pushed,Compare changes" as highlight words on your Slack
+profile settings, so that you can be aware of new commits when somebody pushes
+them.
+
+![Slack configuration](img/slack_configuration.png)
+
+[slackhook]: https://my.slack.com/services/new/incoming-webhook
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 9a5c5a5c92a..a3921f1b89f 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -17,7 +17,7 @@ Public projects can be cloned **without any** authentication.
They will also be listed on the public access directory (`/public`).
-**Any logged in user** will have [Guest](../permissions/permissions.md)
+**Any logged in user** will have [Guest](../user/permissions.md)
permissions on the repository.
### Internal projects
@@ -27,7 +27,7 @@ Internal projects can be cloned by any logged in user.
They will also be listed on the public access directory (`/public`) for logged
in users.
-Any logged in user will have [Guest](../permissions/permissions.md) permissions
+Any logged in user will have [Guest](../user/permissions.md) permissions
on the repository.
### How to change project visibility
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index fa976134341..835af5443a3 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -11,12 +11,13 @@ You can only restore a backup to exactly the same version of GitLab that you cre
on, for example 7.2.1. The best way to migrate your repositories from one server to
another is through backup restore.
-You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json`
-(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
-from source). This file contains the database encryption key used
-for two-factor authentication. If you restore a GitLab backup without
-restoring the database encryption key, users who have two-factor
-authentication enabled will lose access to your GitLab server.
+You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and
+`/etc/gitlab/gitlab.rb` (for omnibus packages) or
+`/home/git/gitlab/config/secrets.yml` (for installations from source). This file
+contains the database encryption keys used for two-factor authentication and CI
+secret variables, among other things. If you restore a GitLab backup without
+restoring the database encryption key, users who have two-factor authentication
+enabled will lose access to your GitLab server.
```
# use this command if you've installed GitLab with the Omnibus package
@@ -221,11 +222,12 @@ of using encryption in the first place!
If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you have an installation from source, please consider backing up your `.secret` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
-At the very **minimum** you should backup `/etc/gitlab/gitlab-secrets.json`
-(Omnibus) or `/home/git/gitlab/.secret` (source) to preserve your
-database encryption key.
+At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
+`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
+`/home/git/gitlab/config/secrets.yml` (source) to preserve your database
+encryption key.
## Restore a previously created backup
@@ -240,11 +242,11 @@ the SQL database it needs to import data into ('gitlabhq_production').
All existing data will be either erased (SQL) or moved to a separate
directory (repositories, uploads).
-If some or all of your GitLab users are using two-factor authentication
-(2FA) then you must also make sure to restore
-`/etc/gitlab/gitlab-secrets.json` (Omnibus) or `/home/git/gitlab/.secret`
-(installations from source). Note that you need to run `gitlab-ctl
-reconfigure` after changing `gitlab-secrets.json`.
+If some or all of your GitLab users are using two-factor authentication (2FA)
+then you must also make sure to restore `/etc/gitlab/gitlab.rb` and
+`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
+`/home/git/gitlab/config/secrets.yml` (installations from source). Note that you
+need to run `gitlab-ctl reconfigure` after changing `gitlab-secrets.json`.
### Installation from source
@@ -382,6 +384,13 @@ backups using all your disk space. To do this add the following lines to
gitlab_rails['backup_keep_time'] = 604800
```
+Note that the `backup_keep_time` configuration option only manages local
+files. GitLab does not automatically prune old files stored in a third-party
+object storage (e.g. AWS S3) because the user may not have permission to list
+and delete files. We recommend that you configure the appropriate retention
+policy for your object storage. For example, you can configure [the S3 backup
+policy here as described here](http://stackoverflow.com/questions/37553070/gitlab-omnibus-delete-backup-from-amazon-s3).
+
NOTE: This cron job does not [backup your omnibus-gitlab configuration](#backup-and-restore-omnibus-gitlab-configuration) or [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
## Alternative backup strategies
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 8fbcbb983e9..cf891cd90ad 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -2,7 +2,7 @@
## Remove garbage from filesystem. Important! Data loss!
-Remove namespaces(dirs) from `/home/git/repositories` if they don't exist in GitLab database.
+Remove namespaces(dirs) from all repository storage paths if they don't exist in GitLab database.
```
# omnibus-gitlab
@@ -12,7 +12,7 @@ sudo gitlab-rake gitlab:cleanup:dirs
bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production
```
-Rename repositories from `/home/git/repositories` if they don't exist in GitLab database.
+Rename repositories from all repository storage paths if they don't exist in GitLab database.
The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created.
```
diff --git a/doc/raketasks/maintenance.md b/doc/raketasks/maintenance.md
index d9dce2af480..315cb56a089 100644
--- a/doc/raketasks/maintenance.md
+++ b/doc/raketasks/maintenance.md
@@ -167,3 +167,22 @@ of those assets. Unless you are modifying the JavaScript / CSS code on your
production machine after installing the package, there should be no reason to redo
rake assets:precompile on the production machine. If you suspect that assets
have been corrupted, you should reinstall the omnibus package.
+
+## Tracking Deployments
+
+GitLab provides a Rake task that lets you track deployments in GitLab
+Performance Monitoring. This Rake task simply stores the current GitLab version
+in the GitLab Performance Monitoring database.
+
+For Omnibus-packages:
+
+```
+sudo gitlab-rake gitlab:track_deployment
+```
+
+For installations from source:
+
+```
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:track_deployment RAILS_ENV=production
+```
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 629d38efc53..8a5e2d6e16b 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -60,8 +60,8 @@ block_auto_created_users: false
## Disable Two-factor Authentication (2FA) for all users
This task will disable 2FA for all users that have it enabled. This can be
-useful if GitLab's `.secret` file has been lost and users are unable to login,
-for example.
+useful if GitLab's `config/secrets.yml` file has been lost and users are unable
+to login, for example.
```bash
# omnibus-gitlab
diff --git a/doc/update/4.0-to-4.1.md b/doc/update/4.0-to-4.1.md
index c163bfd348d..c66c6dd0fd8 100644
--- a/doc/update/4.0-to-4.1.md
+++ b/doc/update/4.0-to-4.1.md
@@ -42,7 +42,7 @@ sudo -u gitlab -H bundle exec rake db:migrate RAILS_ENV=production
sudo mv /etc/init.d/gitlab /etc/init.d/gitlab.old
# get new one using sidekiq
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/4-1-stable/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
```
diff --git a/doc/update/4.2-to-5.0.md b/doc/update/4.2-to-5.0.md
index ee6de51c923..7654f4a0131 100644
--- a/doc/update/4.2-to-5.0.md
+++ b/doc/update/4.2-to-5.0.md
@@ -126,7 +126,7 @@ sudo chmod -R u+rwX /home/git/gitlab/tmp/pids
```bash
# init.d
sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-0-stable/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
# unicorn
diff --git a/doc/update/5.0-to-5.1.md b/doc/update/5.0-to-5.1.md
index f0fddcf83af..c19a819ab5a 100644
--- a/doc/update/5.0-to-5.1.md
+++ b/doc/update/5.0-to-5.1.md
@@ -63,7 +63,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
```bash
# init.d
sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlab-recipes/5-1-stable/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
```
diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md
index c5254f6fb0c..fe8990b6843 100644
--- a/doc/update/5.2-to-5.3.md
+++ b/doc/update/5.2-to-5.3.md
@@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
```bash
sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-3-stable/lib/support/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
```
diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md
index c4a6146dcda..5f82ad7d444 100644
--- a/doc/update/5.3-to-5.4.md
+++ b/doc/update/5.3-to-5.4.md
@@ -71,7 +71,7 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production
```bash
sudo rm /etc/init.d/gitlab
-sudo curl -L --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab
+sudo curl --location --output /etc/init.d/gitlab https://raw.github.com/gitlabhq/gitlabhq/5-4-stable/lib/support/init.d/gitlab
sudo chmod +x /etc/init.d/gitlab
```
diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md
index 236430b5951..5352fd52f93 100644
--- a/doc/update/6.9-to-7.0.md
+++ b/doc/update/6.9-to-7.0.md
@@ -33,7 +33,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
+curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
cd ruby-2.1.2
./configure --disable-install-rdoc
make
diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md
index a4e9be9946e..71f39c44077 100644
--- a/doc/update/7.0-to-7.1.md
+++ b/doc/update/7.0-to-7.1.md
@@ -33,7 +33,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl -L --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
+curl --location --progress ftp://ftp.ruby-lang.org/pub/ruby/2.1/ruby-2.1.2.tar.gz | tar xz
cd ruby-2.1.2
./configure --disable-install-rdoc
make
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 305017b7048..117e2afaaa0 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -71,7 +71,7 @@ sudo -u git -H git checkout v2.6.5
First we download Go 1.5 and install it into `/usr/local/go`:
```bash
-curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
new file mode 100644
index 00000000000..98721763566
--- /dev/null
+++ b/doc/update/8.10-to-8.11.md
@@ -0,0 +1,191 @@
+# From 8.10 to 8.11
+
+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
+
+If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+
+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-11-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-11-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 v3.4.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
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.7.8
+sudo -u git -H make
+```
+
+### 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-10-stable:config/gitlab.yml.example origin/8-11-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-10-stable:lib/support/nginx/gitlab-ssl origin/8-11-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-10-stable:lib/support/nginx/gitlab origin/8-11-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-11-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-11-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
+
+### 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.10)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.9 to 8.10](8.9-to-8.10.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/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index 84065a84e50..a057a423e61 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-10-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.2.0
+sudo -u git -H git checkout v3.2.1
```
### 5. Update gitlab-workhorse
@@ -58,7 +58,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.7.7
+sudo -u git -H git checkout v0.7.8
sudo -u git -H make
```
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
new file mode 100644
index 00000000000..816094bf8d2
--- /dev/null
+++ b/doc/user/account/security.md
@@ -0,0 +1,3 @@
+# Account Security
+
+- [Two-Factor Authentication](two_factor_authentication.md)
diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md
new file mode 100644
index 00000000000..881358ed94d
--- /dev/null
+++ b/doc/user/account/two_factor_authentication.md
@@ -0,0 +1,68 @@
+# Two-Factor Authentication
+
+## Recovery options
+
+If you lose your code generation device (such as your mobile phone) and you need
+to disable two-factor authentication on your account, you have several options.
+
+### Use a saved recovery code
+
+When you enabled two-factor authentication for your account, a series of
+recovery codes were generated. If you saved those codes somewhere safe, you
+may use one to sign in.
+
+First, enter your username/email and password on the GitLab sign in page. When
+prompted for a two-factor code, enter one of the recovery codes you saved
+previously.
+
+> **Note:** Once a particular recovery code has been used, it cannot be used again.
+ You may still use the other saved recovery codes at a later time.
+
+### Generate new recovery codes using SSH
+
+It's not uncommon for users to forget to save the recovery codes when enabling
+two-factor authentication. If you have an SSH key added to your GitLab account,
+you can generate a new set of recovery codes using SSH.
+
+Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
+confirm that you wish to generate new codes. If you choose to continue, any
+previously saved codes will be invalidated.
+
+```bash
+$ ssh git@gitlab.example.com 2fa_recovery_codes
+Are you sure you want to generate new two-factor recovery codes?
+Any existing recovery codes you saved will be invalidated. (yes/no)
+yes
+
+Your two-factor authentication recovery codes are:
+
+119135e5a3ebce8e
+11f6v2a498810dcd
+3924c7ab2089c902
+e79a3398bfe4f224
+34bd7b74adbc8861
+f061691d5107df1a
+169bf32a18e63e7f
+b510e7422e81c947
+20dbed24c5e74663
+df9d3b9403b9c9f0
+
+During sign in, use one of the codes above when prompted for
+your two-factor code. Then, visit your Profile Settings and add
+a new device so you do not lose access to your account again.
+```
+
+Next, go to the GitLab sign in page and enter your username/email and password.
+When prompted for a two-factor code, enter one of the recovery codes obtained
+from the command line output.
+
+> **Note:** After signing in, you should immediately visit your **Profile Settings
+ -> Account** to set up two-factor authentication with a new device.
+
+### Ask a GitLab administrator to disable two-factor on your account
+
+If the above two methods are not possible, you may ask a GitLab global
+administrator to disable two-factor authentication for your account. Please
+be aware that this will temporarily leave your account in a less secure state.
+You should sign in and re-enable two-factor authentication as soon as possible
+after the administrator disables it.
diff --git a/doc/user/admin_area/img/admin_labels.png b/doc/user/admin_area/img/admin_labels.png
new file mode 100644
index 00000000000..1ee33a534ab
--- /dev/null
+++ b/doc/user/admin_area/img/admin_labels.png
Binary files differ
diff --git a/doc/user/admin_area/labels.md b/doc/user/admin_area/labels.md
new file mode 100644
index 00000000000..9e2a89ebdf6
--- /dev/null
+++ b/doc/user/admin_area/labels.md
@@ -0,0 +1,9 @@
+# Labels
+
+## Default Labels
+
+### Define your own default Label Set
+
+Labels that are created within the Labels view on the Admin Dashboard will be automatically added to each new project.
+
+![Default label set](img/admin_labels.png)
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
new file mode 100644
index 00000000000..34e2e557f89
--- /dev/null
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -0,0 +1,20 @@
+# Continuous integration Admin settings
+
+## Maximum artifacts size
+
+The maximum size of the [build artifacts][art-yml] can be set in the Admin area
+of your GitLab instance. The value is in MB and the default is 100MB. Note that
+this setting is set for each build.
+
+1. Go to **Admin area > Settings** (`/admin/application_settings`).
+
+ ![Admin area settings button](img/admin_area_settings_button.png)
+
+1. Change the value of the maximum artifacts size (in MB):
+
+ ![Admin area maximum artifacts size](img/admin_area_maximum_artifacts_size.png)
+
+1. Hit **Save** for the changes to take effect.
+
+
+[art-yml]: ../../../administration/build_artifacts.md
diff --git a/doc/user/admin_area/settings/img/access_restrictions.png b/doc/user/admin_area/settings/img/access_restrictions.png
new file mode 100644
index 00000000000..8eea84320d7
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..53f7e76033e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..509708b627f
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..bd87b73cf9e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..8b00a18320b
--- /dev/null
+++ b/doc/user/admin_area/settings/img/restricted_url.png
Binary files differ
diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md
new file mode 100644
index 00000000000..4b540473a6e
--- /dev/null
+++ b/doc/user/admin_area/settings/sign_up_restrictions.md
@@ -0,0 +1,22 @@
+# Sign-up restrictions
+
+## Blacklist email domains
+
+> [Introduced][ce-5259] in GitLab 8.10.
+
+With this feature enabled, you can block email addresses of a specific domain
+from creating an account on your GitLab server. This is particularly useful to
+prevent spam. Disposable email addresses are usually used by malicious users to
+create dummy accounts and spam issues.
+
+This feature can be activated via the **Application Settings** in the Admin area,
+and you have the option of entering the list manually, or uploading a file with
+the list.
+
+The blacklist accepts wildcards, so you can use `*.test.com` to block every
+`test.com` subdomain, or `*.io` to block all domains ending in `.io`. Domains
+should be separated by a whitespace, semicolon, comma, or a new line.
+
+![Domain Blacklist](img/domain_blacklist.png)
+
+[ce-5259]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5259
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
new file mode 100644
index 00000000000..633f16a617c
--- /dev/null
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -0,0 +1,40 @@
+# Visibility and access controls
+
+## Enabled Git access protocols
+
+> [Introduced][ce-4696] in GitLab 8.10.
+
+With GitLab's Access restrictions you can choose which Git access protocols you
+want your users to use to communicate with GitLab. This feature can be enabled
+via the `Application Settings` in the Admin interface.
+
+The setting is called `Enabled Git access protocols`, and it gives you the option
+to choose between:
+
+- Both SSH and HTTP(S)
+- Only SSH
+- Only HTTP(s)
+
+![Settings Overview](img/access_restrictions.png)
+
+When both SSH and HTTP(S) are enabled, GitLab will behave as usual, it will give
+your users the option to choose which protocol they would like to use.
+
+When you choose to allow only one of the protocols, a couple of things will happen:
+
+- The project page will only show the allowed protocol's URL, with no option to
+ change it.
+- A tooltip will be shown when you hover over the URL's protocol, if an action
+ on the user's part is required, e.g. adding an SSH key, or setting a password.
+
+![Project URL with SSH only access](img/restricted_url.png)
+
+On top of these UI restrictions, GitLab will deny all Git actions on the protocol
+not selected.
+
+> **Note:** Please keep in mind that disabling an access protocol does not actually
+ block access to the server itself. The ports used for the protocol, be it SSH or
+ HTTP, will still be accessible. What GitLab does is restrict access on the
+ application level.
+
+[ce-4696]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4696
diff --git a/doc/markdown/img/logo.png b/doc/user/img/markdown_logo.png
index 05c8b0d0ccf..05c8b0d0ccf 100644
--- a/doc/markdown/img/logo.png
+++ b/doc/user/img/markdown_logo.png
Binary files differ
diff --git a/doc/user/img/markdown_video.mp4 b/doc/user/img/markdown_video.mp4
new file mode 100644
index 00000000000..1fc478842f5
--- /dev/null
+++ b/doc/user/img/markdown_video.mp4
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
new file mode 100644
index 00000000000..c7fda8a497f
--- /dev/null
+++ b/doc/user/markdown.md
@@ -0,0 +1,786 @@
+# 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)
+
+**[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:**
+Not all of the GitLab-specific extensions to Markdown that are described in
+this document currently work on our documentation website.
+>
+For the best result, we encourage you to check this document out as rendered
+by GitLab: [markdown.md]
+
+_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._
+
+GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
+
+You can use GFM in the following areas:
+
+- comments
+- issues
+- merge requests
+- milestones
+- snippets (the snippet must be named with a `.md` extension)
+- wiki pages
+- markdown documents inside the repository
+
+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
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
+
+GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
+
+A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
+
+ Roses are red [followed by two or more spaces]
+ Violets are blue
+
+ Sugar is sweet
+
+Roses are red
+Violets are blue
+
+Sugar is sweet
+
+## 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
+
+It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
+
+ perform_complicated_task
+
+ do_this_and_do_that_and_another_thing
+
+perform_complicated_task
+
+do_this_and_do_that_and_another_thing
+
+## 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
+
+GFM will autolink almost any URL you copy and paste into your text:
+
+ * https://www.google.com
+ * https://google.com/
+ * ftp://ftp.us.debian.org/debian/
+ * smb://foo/bar/baz
+ * irc://irc.freenode.net/gitlab
+ * http://localhost:3000
+
+* https://www.google.com
+* https://google.com/
+* ftp://ftp.us.debian.org/debian/
+* smb://foo/bar/baz
+* irc://irc.freenode.net/gitlab
+* http://localhost:3000
+
+## Multiline Blockquote
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
+
+On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
+GFM supports multiline blockquotes fenced by <code>>>></code>:
+
+```no-highlight
+>>>
+If you paste a message from somewhere else
+
+that
+
+spans
+
+multiple lines,
+
+you can quote that without having to manually prepend `>` to every line!
+>>>
+```
+
+>>>
+If you paste a message from somewhere else
+
+that
+
+spans
+
+multiple lines,
+
+you can quote that without having to manually prepend `>` to every line!
+>>>
+
+## 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
+
+_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
+list of supported languages visit the Rouge website._
+
+Blocks of code are either fenced by lines with three back-ticks <code>```</code>,
+or are indented with four spaces. Only the fenced code blocks support syntax
+highlighting:
+
+```no-highlight
+Inline `code` has `back-ticks around` it.
+```
+
+Inline `code` has `back-ticks around` it.
+
+Example:
+
+ ```javascript
+ var s = "JavaScript syntax highlighting";
+ alert(s);
+ ```
+
+ ```python
+ def function():
+ #indenting works just fine in the fenced code block
+ s = "Python syntax highlighting"
+ print s
+ ```
+
+ ```ruby
+ require 'redcarpet'
+ markdown = Redcarpet.new("Hello World!")
+ puts markdown.to_html
+ ```
+
+ ```
+ No language indicated, so no syntax highlighting.
+ s = "There is no highlighting for this."
+ But let's throw in a <b>tag</b>.
+ ```
+
+becomes:
+
+```javascript
+var s = "JavaScript syntax highlighting";
+alert(s);
+```
+
+```python
+def function():
+ #indenting works just fine in the fenced code block
+ s = "Python syntax highlighting"
+ print s
+```
+
+```ruby
+require 'redcarpet'
+markdown = Redcarpet.new("Hello World!")
+puts markdown.to_html
+```
+
+```
+No language indicated, so no syntax highlighting.
+s = "There is no highlighting for this."
+But let's throw in a <b>tag</b>.
+```
+
+## Inline Diff
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
+
+With inline diffs tags you can display {+ additions +} or [- deletions -].
+
+The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}.
+
+However the wrapping tags cannot be mixed as such:
+
+- {+ additions +]
+- [+ additions +}
+- {- deletions -]
+- [- deletions -}
+
+## Emoji
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
+
+ Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
+
+ :zap: You can use emoji anywhere GFM is supported. :v:
+
+ You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+
+ If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+
+ Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
+
+Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
+
+:zap: You can use emoji anywhere GFM is supported. :v:
+
+You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
+
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+
+Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
+
+## Special GitLab References
+
+GFM recognizes special references.
+
+You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
+
+GFM will turn that reference into a link so you can navigate between them easily.
+
+GFM will recognize the following:
+
+| input | references |
+|:-----------------------|:--------------------------- |
+| `@user_name` | specific user |
+| `@group_name` | specific group |
+| `@all` | entire team |
+| `#123` | issue |
+| `!123` | merge request |
+| `$123` | snippet |
+| `~123` | label by ID |
+| `~bug` | one-word label by name |
+| `~"feature request"` | multi-word label by name |
+| `%123` | milestone by ID |
+| `%v1.23` | one-word milestone by name |
+| `%"release candidate"` | multi-word milestone by name |
+| `9ba12248` | specific commit |
+| `9ba12248...b19a04f5` | commit range comparison |
+| `[README](doc/README)` | repository file references |
+
+GFM also recognizes certain cross-project references:
+
+| input | references |
+|:----------------------------------------|:------------------------|
+| `namespace/project#123` | issue |
+| `namespace/project!123` | merge request |
+| `namespace/project%123` | milestone |
+| `namespace/project$123` | snippet |
+| `namespace/project@9ba12248` | specific commit |
+| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
+| `namespace/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
+
+You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
+
+```no-highlight
+- [x] Completed task
+- [ ] Incomplete task
+ - [ ] Sub-task 1
+ - [x] Sub-task 2
+ - [ ] Sub-task 3
+```
+
+- [x] Completed task
+- [ ] Incomplete task
+ - [ ] Sub-task 1
+ - [x] Sub-task 2
+ - [ ] Sub-task 3
+
+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
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
+
+Image tags with a video extension are automatically converted to a video player.
+
+The valid video extensions are `.mp4`, `.m4v`, `.mov`, `.webm`, and `.ogv`.
+
+ Here's a sample video:
+
+ ![Sample Video](img/markdown_video.mp4)
+
+Here's a sample video:
+
+![Sample Video](img/markdown_video.mp4)
+
+# Standard Markdown
+
+## Headers
+
+```no-highlight
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+Alternatively, for H1 and H2, an underline-ish style:
+
+Alt-H1
+======
+
+Alt-H2
+------
+```
+
+# H1
+## H2
+### H3
+#### H4
+##### H5
+###### H6
+
+Alternatively, for H1 and H2, an underline-ish style:
+
+Alt-H1
+======
+
+Alt-H2
+------
+
+### Header IDs and links
+
+All Markdown-rendered headers automatically get IDs, except in comments.
+
+On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
+
+The IDs are generated from the content of the header according to the following rules:
+
+1. All text is converted to lowercase
+1. All non-word text (e.g., punctuation, HTML) is removed
+1. All spaces are converted to hyphens
+1. Two or more hyphens in a row are converted to one
+1. If a header with the same ID has already been generated, a unique
+ incrementing number is appended, starting at 1.
+
+For example:
+
+```
+# This header has spaces in it
+## This header has a :thumbsup: in it
+# This header has Unicode in it: 한글
+## This header has spaces in it
+### This header has spaces in it
+```
+
+Would generate the following link IDs:
+
+1. `this-header-has-spaces-in-it`
+1. `this-header-has-a-in-it`
+1. `this-header-has-unicode-in-it-한글`
+1. `this-header-has-spaces-in-it`
+1. `this-header-has-spaces-in-it-1`
+
+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
+
+```no-highlight
+Emphasis, aka italics, with *asterisks* or _underscores_.
+
+Strong emphasis, aka bold, with **asterisks** or __underscores__.
+
+Combined emphasis with **asterisks and _underscores_**.
+
+Strikethrough uses two tildes. ~~Scratch this.~~
+```
+
+Emphasis, aka italics, with *asterisks* or _underscores_.
+
+Strong emphasis, aka bold, with **asterisks** or __underscores__.
+
+Combined emphasis with **asterisks and _underscores_**.
+
+Strikethrough uses two tildes. ~~Scratch this.~~
+
+## Lists
+
+```no-highlight
+1. First ordered list item
+2. Another item
+ * Unordered sub-list.
+1. Actual numbers don't matter, just that it's a number
+ 1. Ordered sub-list
+4. And another item.
+
+* Unordered list can use asterisks
+- Or minuses
++ Or pluses
+```
+
+1. First ordered list item
+2. Another item
+ * Unordered sub-list.
+1. Actual numbers don't matter, just that it's a number
+ 1. Ordered sub-list
+4. And another item.
+
+* Unordered list can use asterisks
+- Or minuses
++ Or pluses
+
+If a list item contains multiple paragraphs,
+each subsequent paragraph should be indented with four spaces.
+
+```no-highlight
+1. First ordered list item
+
+ Second paragraph of first item.
+2. Another item
+```
+
+1. First ordered list item
+
+ Second paragraph of first item.
+2. Another item
+
+If the second paragraph isn't indented with four spaces,
+the second list item will be incorrectly labeled as `1`.
+
+```no-highlight
+1. First ordered list item
+
+ Second paragraph of first item.
+2. Another item
+```
+
+1. First ordered list item
+
+ Second paragraph of first item.
+2. Another item
+
+## Links
+
+There are two ways to create links, inline-style and reference-style.
+
+ [I'm an inline-style link](https://www.google.com)
+
+ [I'm a reference-style link][Arbitrary case-insensitive reference text]
+
+ [I'm a relative reference to a repository file](LICENSE)
+
+ [You can use numbers for reference-style link definitions][1]
+
+ Or leave it empty and use the [link text itself][]
+
+ Some text to show that the reference links can follow later.
+
+ [arbitrary case-insensitive reference text]: https://www.mozilla.org
+ [1]: http://slashdot.org
+ [link text itself]: https://www.reddit.com
+
+[I'm an inline-style link](https://www.google.com)
+
+[I'm a reference-style link][Arbitrary case-insensitive reference text]
+
+[I'm a relative reference to a repository file](LICENSE)[^1]
+
+[You can use numbers for reference-style link definitions][1]
+
+Or leave it empty and use the [link text itself][]
+
+Some text to show that the reference links can follow later.
+
+[arbitrary case-insensitive reference text]: https://www.mozilla.org
+[1]: http://slashdot.org
+[link text itself]: https://www.reddit.com
+
+**Note**
+
+Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
+
+`[I'm a reference-style link](style)`
+
+will point the link to `wikis/style` when the link is inside of a wiki markdown file.
+
+## Images
+
+ Here's our logo (hover to see the title text):
+
+ Inline-style:
+ ![alt text](img/markdown_logo.png)
+
+ Reference-style:
+ ![alt text1][logo]
+
+ [logo]: img/markdown_logo.png
+
+Here's our logo:
+
+Inline-style:
+
+![alt text](img/markdown_logo.png)
+
+Reference-style:
+
+![alt text][logo]
+
+[logo]: img/markdown_logo.png
+
+## Blockquotes
+
+```no-highlight
+> Blockquotes are very handy in email to emulate reply text.
+> This line is part of the same quote.
+
+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.
+```
+
+> Blockquotes are very handy in email to emulate reply text.
+> This line is part of the same quote.
+
+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
+
+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.
+
+```no-highlight
+<dl>
+ <dt>Definition list</dt>
+ <dd>Is something people use sometimes.</dd>
+
+ <dt>Markdown in HTML</dt>
+ <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
+</dl>
+```
+
+<dl>
+ <dt>Definition list</dt>
+ <dd>Is something people use sometimes.</dd>
+
+ <dt>Markdown in HTML</dt>
+ <dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
+</dl>
+
+## Horizontal Rule
+
+```
+Three or more...
+
+---
+
+Hyphens
+
+***
+
+Asterisks
+
+___
+
+Underscores
+```
+
+Three or more...
+
+---
+
+Hyphens
+
+***
+
+Asterisks
+
+___
+
+Underscores
+
+## 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.
+
+Here are some things to try out:
+
+```
+Here's a line for us to start with.
+
+This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
+
+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 on its own line, because the previous line ends with two
+spaces.
+```
+
+Here's a line for us to start with.
+
+This line is separated from the one above by two newlines, so it will be a *separate paragraph*.
+
+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 on its own line, because the previous line ends with two
+spaces.
+
+## Tables
+
+Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
+
+```
+| header 1 | header 2 |
+| -------- | -------- |
+| cell 1 | cell 2 |
+| cell 3 | cell 4 |
+```
+
+Code above produces next output:
+
+| header 1 | header 2 |
+| -------- | -------- |
+| cell 1 | cell 2 |
+| cell 3 | cell 4 |
+
+**Note**
+
+The row of dashes between the table header and body must have at least three dashes in each column.
+
+By including colons in the header row, you can align the text within that column:
+
+```
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
+| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+```
+
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
+| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+
+
+## Wiki-specific Markdown
+
+The following examples show how links inside wikis behave.
+
+### Wiki - Direct page link
+
+A link which just includes the slug for a page will point to that page,
+_at the base level of the wiki_.
+
+This snippet would link to a `documentation` page at the root of your wiki:
+
+```markdown
+[Link to Documentation](documentation)
+```
+
+### Wiki - Direct file link
+
+Links with a file extension point to that file, _relative to the current page_.
+
+If this snippet was placed on a page at `<your_wiki>/documentation/related`,
+it would link to `<your_wiki>/documentation/file.md`:
+
+```markdown
+[Link to File](file.md)
+```
+
+### Wiki - Hierarchical link
+
+A link can be constructed relative to the current wiki page using `./<page>`,
+`../<page>`, etc.
+
+- 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)
+ ```
+
+- 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)
+ ```
+
+- 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)
+ ```
+
+- 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)
+ ```
+
+### Wiki - Root link
+
+A link starting with a `/` is relative to the wiki root.
+
+- This snippet links to `<wiki_root>/documentation`:
+
+ ```markdown
+ [Link to Related Page](/documentation)
+ ```
+
+- This snippet links to `<wiki_root>/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.
+
+[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
+[rouge]: http://rouge.jneen.net/ "Rouge website"
+[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
+[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
new file mode 100644
index 00000000000..66542861781
--- /dev/null
+++ b/doc/user/permissions.md
@@ -0,0 +1,131 @@
+# Permissions
+
+Users have different abilities depending on the access level they have in a
+particular group or project. If a user is both in a group's project and the
+project itself, the highest permission level is used.
+
+On public and internal projects the Guest role is not enforced. All users will
+be able to create issues, leave comments, and pull or download the project code.
+
+GitLab administrators receive all permissions.
+
+To add or import a user, you can follow the [project users and members
+documentation](../workflow/add-user/add-user.md).
+
+## Project
+
+The following table depicts the various user permission levels in a project.
+
+| Action | Guest | Reporter | Developer | Master | Owner |
+|---------------------------------------|---------|------------|-------------|----------|--------|
+| Create new issue | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Leave comments | ✓ | ✓ | ✓ | ✓ | ✓ |
+| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| Pull project code | | ✓ | ✓ | ✓ | ✓ |
+| Download project | | ✓ | ✓ | ✓ | ✓ |
+| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
+| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
+| Manage labels | | ✓ | ✓ | ✓ | ✓ |
+| See a commit status | | ✓ | ✓ | ✓ | ✓ |
+| See a container registry | | ✓ | ✓ | ✓ | ✓ |
+| See environments | | ✓ | ✓ | ✓ | ✓ |
+| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
+| Create new merge request | | | ✓ | ✓ | ✓ |
+| Create new branches | | | ✓ | ✓ | ✓ |
+| Push to non-protected branches | | | ✓ | ✓ | ✓ |
+| Force push to non-protected branches | | | ✓ | ✓ | ✓ |
+| Remove non-protected branches | | | ✓ | ✓ | ✓ |
+| Add tags | | | ✓ | ✓ | ✓ |
+| Write a wiki | | | ✓ | ✓ | ✓ |
+| Cancel and retry builds | | | ✓ | ✓ | ✓ |
+| 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 | | | | ✓ | ✓ |
+| Enable/disable branch protection | | | | ✓ | ✓ |
+| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
+| Rewrite/remove Git tags | | | | ✓ | ✓ |
+| Edit project | | | | ✓ | ✓ |
+| Add deploy keys to project | | | | ✓ | ✓ |
+| Configure project hooks | | | | ✓ | ✓ |
+| Manage runners | | | | ✓ | ✓ |
+| Manage build triggers | | | | ✓ | ✓ |
+| Manage variables | | | | ✓ | ✓ |
+| Delete environments | | | | ✓ | ✓ |
+| Switch visibility level | | | | | ✓ |
+| Transfer project to another namespace | | | | | ✓ |
+| Remove project | | | | | ✓ |
+| Force push to protected branches [^2] | | | | | |
+| Remove protected branches [^2] | | | | | |
+
+[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
+
+## Group
+
+Any user can remove themselves from a group, unless they are the last Owner of
+the group. The following table depicts the various user permission levels in a
+group.
+
+| Action | Guest | Reporter | Developer | Master | Owner |
+|-------------------------|-------|----------|-----------|--------|-------|
+| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
+| Edit group | | | | | ✓ |
+| Create project in group | | | | ✓ | ✓ |
+| Manage group members | | | | | ✓ |
+| Remove group | | | | | ✓ |
+
+## External Users
+
+In cases where it is desired that a user has access only to some internal or
+private projects, there is the option of creating **External Users**. This
+feature may be useful when for example a contractor is working on a given
+project and should only have access to that project.
+
+External users can only access projects to which they are explicitly granted
+access, thus hiding all other internal or private ones from them. Access can be
+granted by adding the user as member to the project or group.
+
+They will, like usual users, receive a role in the project or group with all
+the abilities that are mentioned in the table above. They cannot however create
+groups or projects, and they have the same access as logged out users in all
+other cases.
+
+An administrator can flag a user as external [through the API](../api/users.md)
+or by checking the checkbox on the admin panel. As an administrator, navigate
+to **Admin > Users** to create a new user or edit an existing one. There, you
+will find the option to flag the user as external.
+
+By default new users are not set as external users. This behavior can be changed
+by an administrator under **Admin > Application Settings**.
+
+## GitLab CI
+
+GitLab CI permissions rely on the role the user has in GitLab. There are four
+permission levels it total:
+
+- admin
+- master
+- developer
+- guest/reporter
+
+The admin user can perform any action on GitLab CI in scope of the GitLab
+instance and project. In addition, all admins can use the admin interface under
+`/admin/runners`.
+
+| Action | Guest, Reporter | Developer | Master | Admin |
+|---------------------------------------|-----------------|-------------|----------|--------|
+| See commits and builds | ✓ | ✓ | ✓ | ✓ |
+| Retry or cancel build | | ✓ | ✓ | ✓ |
+| Remove project | | | ✓ | ✓ |
+| Create project | | | ✓ | ✓ |
+| Change project configuration | | | ✓ | ✓ |
+| Add specific runners | | | ✓ | ✓ |
+| Add shared runners | | | | ✓ |
+| See events in the system | | | | ✓ |
+| Admin interface | | | | ✓ |
diff --git a/doc/user/project/builds/artifacts.md b/doc/user/project/builds/artifacts.md
new file mode 100644
index 00000000000..c93ae1c369c
--- /dev/null
+++ b/doc/user/project/builds/artifacts.md
@@ -0,0 +1,104 @@
+# Introduction to build artifacts
+
+>**Notes:**
+>- Since GitLab 8.2 and GitLab Runner 0.7.0, build artifacts that are created by
+ GitLab Runner are uploaded to GitLab and are downloadable as a single archive
+ (`tar.gz`) using the GitLab UI.
+>- Starting from GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format
+ changed to `ZIP`, and it is now possible to browse its contents, with the added
+ ability of downloading the files separately.
+>- The artifacts browser will be available only for new artifacts that are sent
+ to GitLab using GitLab Runner version 1.0 and up. It will not be possible to
+ browse old artifacts already uploaded to GitLab.
+>- This is the user documentation. For the administration guide see
+ [administration/build_artifacts.md](../../../administration/build_artifacts.md).
+
+Artifacts is a list of files and directories which are attached to a build
+after it completes successfully. This feature is enabled by default in all GitLab installations.
+
+## Defining artifacts in `.gitlab-ci.yml`
+
+A simple example of using the artifacts definition in `.gitlab-ci.yml` would be
+the following:
+
+```yaml
+pdf:
+ script: xelatex mycv.tex
+ artifacts:
+ paths:
+ - mycv.pdf
+```
+
+A job named `pdf` calls the `xelatex` command in order to build a pdf file from
+the latex source file `mycv.tex`. We then define the `artifacts` paths which in
+turn are defined with the `paths` keyword. All paths to files and directories
+are relative to the repository that was cloned during the build.
+
+For more examples on artifacts, follow the artifacts reference in
+[`.gitlab-ci.yml` documentation](../../../ci/yaml/README.md#artifacts).
+
+## Browsing build artifacts
+
+When GitLab receives an artifacts archive, an archive metadata file is also
+generated. This metadata file describes all the entries that are located in the
+artifacts archive itself. The metadata file is in a binary format, with
+additional GZIP compression.
+
+GitLab does not extract the artifacts archive in order to save space, memory
+and disk I/O. It instead inspects the metadata file which contains all the
+relevant information. This is especially important when there is a lot of
+artifacts, or an archive is a very large file.
+
+---
+
+After a build finishes, if you visit the build's specific page, you can see
+that there are two buttons. One is for downloading the artifacts archive and
+the other for browsing its contents.
+
+![Build artifacts browser button](img/build_artifacts_browser_button.png)
+
+---
+
+The archive browser shows the name and the actual file size of each file in the
+archive. If your artifacts contained directories, then you are also able to
+browse inside them.
+
+Below you can see how browsing looks like. In this case we have browsed inside
+the archive and at this point there is one directory and one HTML file.
+
+![Build artifacts browser](img/build_artifacts_browser.png)
+
+---
+
+## Downloading build artifacts
+
+>**Note:**
+GitLab does not extract the entire artifacts archive to send just a single file
+to the user. When clicking on a specific file, [GitLab Workhorse] extracts it
+from the archive and the download begins. This implementation saves space,
+memory and disk I/O.
+
+If you need to download the whole archive, there are buttons in various places
+inside GitLab that make that possible.
+
+1. While on the pipelines page, you can see the download icon for each build's
+ artifacts archive in the right corner:
+
+ ![Build artifacts in Pipelines page](img/build_artifacts_pipelines_page.png)
+
+1. While on the builds page, you can see the download icon for each build's
+ artifacts archive in the right corner:
+
+ ![Build artifacts in Builds page](img/build_artifacts_builds_page.png)
+
+1. While inside a specific build, you are presented with a download button
+ along with the one that browses the archive:
+
+ ![Build artifacts browser button](img/build_artifacts_browser_button.png)
+
+1. And finally, when browsing an archive you can see the download button at
+ the top right corner:
+
+ ![Build artifacts browser](img/build_artifacts_browser.png)
+
+[gitlab workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse "GitLab Workhorse repository"
diff --git a/doc/user/project/builds/img/build_artifacts_browser.png b/doc/user/project/builds/img/build_artifacts_browser.png
new file mode 100644
index 00000000000..d95e2800c0f
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..463540634e3
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..db78386ba7b
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..6c2d1a4bdc7
--- /dev/null
+++ b/doc/user/project/builds/img/build_artifacts_pipelines_page.png
Binary files differ
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
new file mode 100644
index 00000000000..ea7496af089
--- /dev/null
+++ b/doc/user/project/description_templates.md
@@ -0,0 +1,42 @@
+# Description templates
+
+>[Introduced][ce-4981] in GitLab 8.11.
+
+Description templates allow you to define context-specific templates for issue
+and merge request description fields for your project.
+
+## Overview
+
+By using the description templates, users that create a new issue or merge
+request can select a description template to help them communicate with other
+contributors effectively.
+
+Every GitLab project can define its own set of description templates as they
+are added to the root directory of a GitLab project's repository.
+
+Description templates must be written in [Markdown](../markdown.md) and stored
+in your project's repository under a directory named `.gitlab`. Only the
+templates of the default branch will be taken into account.
+
+## Creating issue templates
+
+Create a new Markdown (`.md`) file inside the `.gitlab/issue_templates/`
+directory in your repository. Commit and push to your default branch.
+
+## Creating merge request templates
+
+Similarly to issue templates, create a new Markdown (`.md`) file inside the
+`.gitlab/merge_request_templates/` directory in your repository. Commit and
+push to your default branch.
+
+## Using the templates
+
+Let's take for example that you've created the file `.gitlab/issue_templates/Bug.md`.
+This will enable the `Bug` dropdown option when creating or editing issues. When
+`Bug` is selected, the content from the `Bug.md` template file will be copied
+to the issue description field. The 'Reset template' button will discard any
+changes you made after picking the template and return it to its initial status.
+
+![Description templates](img/description_templates.png)
+
+[ce-4981]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4981
diff --git a/doc/user/project/img/description_templates.png b/doc/user/project/img/description_templates.png
new file mode 100644
index 00000000000..c41cc77a94c
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..63c269f6dbc
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..2b8c10eaa0a
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..112ea171539
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..b69ef034954
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..b757faeb230
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..f8cc81834c4
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..a04cd5aff99
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..2a0dd296480
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..3e133c50327
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..fd25e32f648
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..fd5aaff75f5
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..c0ae0ee9918
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..7c629d9b05e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..c958cf8f224
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..f91ee0f74cc
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..502817a2a46
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..7f408c980b5
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..2a4e3c87fc8
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..52159440f62
--- /dev/null
+++ b/doc/user/project/img/koding_start-build.png
Binary files differ
diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png
new file mode 100644
index 00000000000..57ca2ac5f9e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..26328143717
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..812cc8767b7
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..cc61df7ca97
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..f33f1b2bdb6
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..30ce53f704e
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..1585dde5b29
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..cac926b3e28
--- /dev/null
+++ b/doc/user/project/issue_board.md
@@ -0,0 +1,187 @@
+# Issue board
+
+> [Introduced][ce-5554] in GitLab 8.11.
+
+The GitLab Issue Board is a software project management tool used to plan,
+organize, and visualize a workflow for a feature or product release.
+It can be seen like a light version of a [Kanban] or a [Scrum] board.
+
+Other interesting links:
+
+- [GitLab Issue Board landing page on about.gitlab.com][landing]
+- [YouTube video introduction to Issue Boards][youtube]
+
+## Overview
+
+The Issue Board builds on GitLab's existing issue tracking functionality and
+leverages the power of [labels] by utilizing them as lists of the scrum board.
+
+With the Issue Board you can have a different view of your issues while also
+maintaining the same filtering and sorting abilities you see across the
+issue tracker.
+
+Below is a table of the definitions used for GitLab's Issue Board.
+
+| What we call it | What it means |
+| -------------- | ------------- |
+| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
+| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
+| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
+
+There are three types of lists, the ones you create based on your labels, and
+two default:
+
+- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
+- Label list: a list based on a label. It shows all opened or closed issues with that label.
+
+![GitLab Issue Board](img/issue_board.png)
+
+---
+
+In short, here's a list of actions you can take in an Issue Board:
+
+- [Create a new list](#creating-a-new-list).
+- [Delete an existing list](#deleting-a-list).
+- Drag issues between lists.
+- Drag and reorder the lists themselves.
+- Change issue labels on-the-fly while dragging issues between lists.
+- Close an issue if you drag it to the **Done** list.
+- Create a new list from a non-existing label by [creating the label on-the-fly](#creating-a-new-list)
+ within the Issue Board.
+- [Filter issues](#filtering-issues) that appear across your Issue Board.
+
+If you are not able to perform one or more of the things above, make sure you
+have the right [permissions](#permissions).
+
+## First time using the Issue Board
+
+The first time you navigate to your Issue Board, you will be presented with the
+two default lists (**Backlog** and **Done**) and a welcoming message that gives
+you two options. You can either create a predefined set of labels and create
+their corresponding lists to the Issue Board or opt-out and use your own lists.
+
+![Issue Board welcome message](img/issue_board_welcome_message.png)
+
+If you choose to use and create the predefined lists, they will appear as empty
+because the labels associated to them will not exist up until that moment,
+which means the system has no way of populating them automatically. That's of
+course if the predefined labels don't already exist. If any of them does exist,
+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
+right corner of the Issue Board.
+
+![Issue Board welcome message](img/issue_board_add_list.png)
+
+Simply choose the label to create the list from. The new list will be inserted
+at the end of the lists, before **Done**. Moving and reordering lists is as
+easy as dragging them around.
+
+To create a list for a label that doesn't yet exist, simply create the label by
+choosing **Create new label**. The label will be created on-the-fly and it will
+be immediately added to the dropdown. You can now choose it to create a list.
+
+## Deleting a list
+
+To delete a list from the Issue Board use the small trash icon that is present
+in the list's heading. A confirmation dialog will appear for you to confirm.
+
+Deleting a list doesn't have any effect in issues and labels, it's just the
+list view that is removed. You can always add it back later if you need.
+
+## Searching issues in the Backlog list
+
+The very first time you start using the Issue Board, it is very likely your
+issue tracker is already populated with labels and issues. In that case,
+**Backlog** will have all the issues that don't belong to another list, and
+**Done** will have all the closed ones.
+
+For performance and visibility reasons, each list shows the first 20 issues
+by default. If you have more than 20, you have to start scrolling down for the
+next 20 issues to appear. This can be cumbersome if your issue tracker hosts
+hundreds of issues, and for that reason it is easier to search for issues to
+move from **Backlog** to another list.
+
+Start typing in the search bar under the **Backlog** list and the relevant
+issues will appear.
+
+![Issue Board search Backlog](img/issue_board_search_backlog.png)
+
+## Filtering issues
+
+You should be able to use the filters on top of your Issue Board to show only
+the results you want. This is similar to the filtering used in the issue tracker
+since the metadata from the issues and labels are re-used in the Issue Board.
+
+You can filter by author, assignee, milestone and label.
+
+## Creating workflows
+
+By reordering your lists, you can create workflows. As lists in Issue Boards are
+based on labels, it works out of the box with your existing issues. So if you've
+already labeled things with 'Backend' and 'Frontend', the issue will appear in
+the lists as you create them. In addition, this means you can easily move
+something between lists by changing a label.
+
+A typical workflow of using the Issue Board would be:
+
+1. You have [created][create-labels] and [prioritized][label-priority] labels
+ so that you can easily categorize your issues.
+1. You have a bunch of issues (ideally labeled).
+1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
+ create a workflow.
+1. You move issues around in lists so that your team knows who should be working
+ on what issue.
+1. When the work by one team is done, the issue can be dragged to the next list
+ so someone else can pick up.
+1. When the issue is finally resolved, the issue is moved to the **Done** list
+ and gets automatically closed.
+
+For instance you can create a list based on the label of 'Frontend' and one for
+'Backend'. A designer can start working on an issue by dragging it from
+**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being
+worked on by the designers. Then, once they're done, all they have to do is
+drag it over to the next list, 'Backend', where a backend developer can
+eventually pick it up. Once they’re done, they move it to **Done**, to close the
+issue.
+
+This process can be seen clearly when visiting an issue since with every move
+to another list the label changes and a system not is recorded.
+
+![Issue Board system notes](img/issue_board_system_notes.png)
+
+## Permissions
+
+[Developers and up](../permissions.md) can use all the functionality of the
+Issue Board, that is create/delete lists and drag issues around.
+
+## Tips
+
+A few things to remember:
+
+- The label that corresponds to a list is hidden for issues under that list.
+- Moving an issue between lists removes the label from the list it came from
+ and adds the label from the list it goes to.
+- When moving a card to **Done**, the label of the list it came from is removed
+ and the issue gets closed.
+- An issue can exist in multiple lists if it has more than one label.
+- Lists are populated with issues automatically if the issues are labeled.
+- Clicking on the issue title inside a card will take you to that issue.
+- Clicking on a label inside a card will quickly filter the entire Issue Board
+ and show only the issues from all lists that have that label.
+- Issues inside lists are [ordered by priority][label-priority].
+- For performance and visibility reasons, each list shows the first 20 issues
+ by default. If you have more than 20 issues start scrolling down and the next
+ 20 will appear.
+
+[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554
+[labels]: ./labels.md
+[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development)
+[kanban]: https://en.wikipedia.org/wiki/Kanban_(development)
+[create-labels]: ./labels.md#create-new-labels
+[label-priority]: ./labels.md#prioritize-labels
+[landing]: https://about.gitlab.com/solutions/issueboard
+[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8
diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md
new file mode 100644
index 00000000000..c56a1efe3c2
--- /dev/null
+++ b/doc/user/project/koding.md
@@ -0,0 +1,128 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through using Koding integration on GitLab in
+detail. For configuring and installing please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+You can use Koding integration to run and develop your projects on GitLab. This
+will allow you and the users to test your project without leaving the browser.
+Koding handles projects as stacks which are basic recipes to define your
+environment for your project. With this integration you can automatically
+create a proper stack template for your projects. Currently auto-generated
+stack templates are designed to work with AWS which requires a valid AWS
+credential to be able to use these stacks. You can find more information about
+stacks and the other providers that you can use on Koding following the
+[Koding documentation][koding-docs].
+
+## Enable Integration
+
+You can enable Koding integration by providing the running Koding instance URL
+in Application Settings under **Admin area > Settings** (`/admin/application_settings`).
+
+![Enable Koding](img/koding_enable-koding.png)
+
+Once enabled you will see `Koding` link on your sidebar which leads you to
+Koding Landing page.
+
+![Koding Landing](img/koding_landing.png)
+
+You can navigate to running Koding instance from here. For more information and
+details about configuring the integration, please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+## Set up Koding on Projects
+
+Once it's enabled, you will see some integration buttons on Project pages,
+Merge Requests etc. To get started working on a specific project you first need
+to create a `.koding.yml` file under your project root. You can easily do that
+by using `Set Up Koding` button which will be visible on every project's
+landing page;
+
+![Set Up Koding](img/koding_set-up-ide.png)
+
+Once you click this will open a New File page on GitLab with auto-generated
+`.koding.yml` content based on your server and repository configuration.
+
+![Commit .koding.yml](img/koding_commit-koding.yml.png)
+
+
+## Run a project on Koding
+
+If there is `.koding.yml` exists in your project root, you will see
+`Run in IDE (Koding)` button in your project landing page. You can initiate the
+process from here.
+
+![Run on Koding](img/koding_run-in-ide.png)
+
+This will open Koding defined in the settings in a new window and will start
+importing the project's stack file.
+
+![Import Stack](img/koding_stack-import.png)
+
+You should see the details of your repository imported into your Koding
+instance. Once it's completed it will lead you to the Stack Editor and from
+there you can start using your new stack integrated with your project on your
+GitLab instance. For details about what's next you can follow
+[this guide](https://www.koding.com/docs/creating-an-aws-stack) from step 8.
+
+Once stack initialized you will see the `README.md` content from your project
+in `Stack Build` wizard, this wizard will let you build the stack and import
+your project into it. **Once it's completed it will automatically open the
+related vm instead of importing from scratch**.
+
+![Stack Building](img/koding_start-build.png)
+
+This will take time depending on the required environment.
+
+![Stack Building in Progress](img/koding_build-in-progress.png)
+
+It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
+AWS region. (`t2.nano` is default vm type on auto-generated stack template
+which can be manually changed).
+
+![Stack Building Success](img/koding_build-success.png)
+
+You can check out the `Build Logs` from this success modal as well.
+
+![Stack Build Logs](img/koding_build-logs.png)
+
+You can now `Start Coding`!
+
+![Edit On IDE](img/koding_edit-on-ide.png)
+
+## Try a Merge Request on IDE
+
+It's also possible to try a change on IDE before merging it. This flow only
+enabled if the target project has `.koding.yml` in it's target branch. You
+should see the alternative version of `Run in IDE (Koding)` button in merge
+request pages as well;
+
+![Run in IDE on MR](img/koding_run-mr-in-ide.png)
+
+This will again take you to Koding with proper arguments passed, which will
+allow Koding to modify the stack template provided by target branch. You can
+see the difference;
+
+![Different Branch for MR](img/koding_different-stack-on-mr-try.png)
+
+The flow for the branch stack is also same with the regular project flow.
+
+## Open GitLab from Koding
+
+Since stacks generated with import flow defined in previous steps, they have
+information about the repository they are belonging to. By using this
+information you can access to related GitLab page from stacks on your sidebar
+on Koding.
+
+![Open GitLab from Koding](img/koding_open-gitlab-from-koding.png)
+
+## Other links
+
+- [YouTube video on GitLab + Koding workflow][youtube]
+- [Koding documentation][koding-docs]
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
+[youtube]: https://youtu.be/3wei5yv_Ye8
+[koding-docs]: https://www.koding.com/docs
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index 4258185b7d0..cf1d9cbe69c 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -1,8 +1,8 @@
# Labels
Labels provide an easy way to categorize the issues or merge requests based on
-descriptive titles like `bug`, `documentation` or any other text you feel like
-it. They can have different colors, a description, and are visible throughout
+descriptive titles like `bug`, `documentation` or any other text you feel like.
+They can have different colors, a description, and are visible throughout
the issue tracker or inside each issue individually.
With labels, you can navigate the issue tracker and filter any bloated
@@ -22,34 +22,47 @@ created yet.
![Generate new labels](img/labels_generate.png)
+Creating a new label from scratch is as easy as pressing the **New label**
+button. From there on you can choose the name, give it an optional description,
+a color and you are set.
+
+When you are ready press the **Create label** button to create the new label.
+
+![New label](img/labels_new_label.png)
+
---
-You can skip that and create a new label or click that link and GitLab will
-generate a set of predefined labels for you. There 8 default generated labels
+## Default Labels
+
+It's possible to populate the labels for your project from a set of predefined labels.
+
+### Generate GitLab's predefined label set
+
+![Generate new labels](img/labels_generate.png)
+
+Click the link to 'Generate a default set of labels' and GitLab will
+generate a set of predefined labels for you. There are 8 default generated labels
in total and you can see them in the screenshot below.
![Default generated labels](img/labels_default.png)
---
-You can see that from the labels page you can have an overview of the number of
-issues and merge requests assigned to each label.
-
-Creating a new label from scratch is as easy as pressing the **New label**
-button. From there on you can choose the name, give it an optional description,
-a color and you are set.
+## Labels Overview
-When you are ready press the **Create label** button to create the new label.
+![Default generated labels](img/labels_default.png)
-![New label](img/labels_new_label.png)
+You can see that from the labels page you can have an overview of the number of
+issues and merge requests assigned to each label.
## Prioritize labels
>**Notes:**
- - This feature was introduced in GitLab 8.9.
- - Priority sorting is based on the highest priority label only. This might
- change in the future, follow the discussion in
- https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
+>
+> - Introduced in GitLab 8.9.
+> - Priority sorting is based on the highest priority label only. This might
+> change in the future, follow the discussion in
+> https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
Prioritized labels are like any other label, but sorted by priority. This allows
you to sort issues and merge requests by priority.
@@ -87,8 +100,7 @@ important.
## Create a new label right from the issue tracker
->**Note:**
-This feature was introduced in GitLab 8.6.
+> Introduced in GitLab 8.6.
There are times when you are already in the issue tracker searching for a
label, only to realize it doesn't exist. Instead of going to the **Labels**
diff --git a/doc/user/project/merge_requests/img/conflict_section.png b/doc/user/project/merge_requests/img/conflict_section.png
new file mode 100644
index 00000000000..842e50b14b2
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..83bb60acce2
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..85428129ac8
--- /dev/null
+++ b/doc/user/project/merge_requests/img/discussions_resolved.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
new file mode 100644
index 00000000000..ffb96b17b07
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_widget.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
new file mode 100644
index 00000000000..2c4ab2f5d53
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..73f265bb101
--- /dev/null
+++ b/doc/user/project/merge_requests/img/resolve_discussion_button.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
new file mode 100644
index 00000000000..2559f5f5250
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_request_discussion_resolution.md
@@ -0,0 +1,40 @@
+# Merge Request discussion resolution
+
+> [Introduced][ce-5022] in GitLab 8.11.
+
+Discussion resolution helps keep track of progress during code review.
+Resolving comments prevents you from forgetting to address feedback and lets you
+hide discussions that are no longer relevant.
+
+!["A discussion between two people on a piece of code"][discussion-view]
+
+Comments and discussions can be resolved by anyone with at least Developer
+access to the project, as well as by the author of the merge request.
+
+## Marking a comment or discussion as resolved
+
+You can mark a discussion as resolved by clicking the "Resolve discussion"
+button at the bottom of the discussion.
+
+!["Resolve discussion" button][resolve-discussion-button]
+
+Alternatively, you can mark each comment as resolved individually.
+
+!["Resolve comment" button][resolve-comment-button]
+
+## Jumping between unresolved discussions
+
+When a merge request has a large number of comments it can be difficult to track
+what remains unresolved. You can jump between unresolved discussions with the
+Jump button next to the Reply field on a discussion.
+
+You can also jump to the first unresolved discussion from the button next to the
+resolved discussions tracker.
+
+!["3/4 discussions resolved"][discussions-resolved]
+
+[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
+[resolve-discussion-button]: img/resolve_discussion_button.png
+[resolve-comment-button]: img/resolve_comment_button.png
+[discussion-view]: img/discussion_view.png
+[discussions-resolved]: img/discussions_resolved.png
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
new file mode 100644
index 00000000000..4d7225bd820
--- /dev/null
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -0,0 +1,42 @@
+# Merge conflict resolution
+
+> [Introduced][ce-5479] in GitLab 8.11.
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI. (See
+[conflicts available for resolution](#conflicts-available-for-resolution) for
+more information on when this is available.) If this is an option, you will see
+a **resolve these conflicts** link in the merge request widget:
+
+![Merge request widget](img/merge_request_widget.png)
+
+Clicking this will show a list of files with conflicts, with conflict sections
+highlighted:
+
+![Conflict section](img/conflict_section.png)
+
+Once all conflicts have been marked as using 'ours' or 'theirs', the conflict
+can be resolved. This will perform a merge of the target branch of the merge
+request into the source branch, resolving the conflicts using the options
+chosen. If the source branch is `feature` and the target branch is `master`,
+this is similar to performing `git checkout feature; git merge master` locally.
+
+## Conflicts available for resolution
+
+GitLab allows resolving conflicts in a file where all of the below are true:
+
+- The file is text, not binary
+- The file is in a UTF-8 compatible encoding
+- The file does not already contain conflict markers
+- The file, with conflict markers added, is not over 200 KB in size
+- The file exists under the same path in both branches
+
+If any file with conflicts in that merge request does not meet all of these
+criteria, the conflicts for that merge request cannot be resolved in the UI.
+
+Additionally, GitLab does not detect conflicts in renames away from a path. For
+example, this will not create a conflict: on branch `a`, doing `git mv file1
+file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be
+present in the branch after the merge request is merged.
+
+[ce-5479]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5479
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
new file mode 100644
index 00000000000..f7a686d2ccf
--- /dev/null
+++ b/doc/user/project/protected_branches.md
@@ -0,0 +1,116 @@
+# Protected Branches
+
+[Permissions](../permissions.md) in GitLab are fundamentally defined around the
+idea of having read or write permission to the repository and branches. To
+prevent people from messing with history or pushing code without review, we've
+created protected branches.
+
+## Overview
+
+By default, a protected branch does four simple things:
+
+- it prevents its creation, if not already created, from everybody except users
+ with Master permission
+- it prevents pushes from everybody except users with Master permission
+- it prevents **anyone** from force pushing to the branch
+- it prevents **anyone** from deleting the branch
+
+See the [Changelog](#changelog) section for changes over time.
+
+>
+>Additional functionality for GitLab Enterprise Edition:
+>
+>- Restrict push and merge access to [certain users][ee-restrict]
+
+## Configuring protected branches
+
+To protect a branch, you need to have at least Master permission level. Note
+that the `master` branch is protected by default.
+
+1. Navigate to the main page of the project.
+1. In the upper right corner, click the settings wheel and select **Protected branches**.
+
+ ![Project settings list](img/project_settings_list.png)
+
+1. From the **Branch** dropdown menu, select the branch you want to protect and
+ click **Protect**. In the screenshot below, we chose the `develop` branch.
+
+ ![Protected branches page](img/protected_branches_page.png)
+
+1. Once done, the protected branch will appear in the "Protected branches" list.
+
+ ![Protected branches list](img/protected_branches_list.png)
+
+## Using the Allowed to merge and Allowed to push settings
+
+> [Introduced][ce-5081] in GitLab 8.11.
+
+Since GitLab 8.11, we added another layer of branch protection which provides
+more granular management of protected branches. The "Developers can push"
+option was replaced by an "Allowed to push" setting which can be set to
+allow/prohibit Masters and/or Developers to push to a protected branch.
+
+Using the "Allowed to push" and "Allowed to merge" settings, you can control
+the actions that different roles can perform with the protected branch.
+For example, you could set "Allowed to push" to "No one", and "Allowed to merge"
+to "Developers + Masters", to require _everyone_ to submit a merge request for
+changes going into the protected branch. This is compatible with workflows like
+the [GitLab workflow](../../workflow/gitlab_flow.md).
+
+However, there are workflows where that is not needed, and only protecting from
+force pushes and branch removal is useful. For those workflows, you can allow
+everyone with write access to push to a protected branch by setting
+"Allowed to push" to "Developers + Masters".
+
+You can set the "Allowed to push" and "Allowed to merge" options while creating
+a protected branch or afterwards by selecting the option you want from the
+dropdown list in the "Already protected" area.
+
+![Developers can push](img/protected_branches_devs_can_push.png)
+
+If you don't choose any of those options while creating a protected branch,
+they are set to "Masters" by default.
+
+## Wildcard protected branches
+
+> [Introduced][ce-4665] in GitLab 8.10.
+
+You can specify a wildcard protected branch, which will protect all branches
+matching the wildcard. For example:
+
+| Wildcard Protected Branch | Matching Branches |
+|---------------------------+--------------------------------------------------------|
+| `*-stable` | `production-stable`, `staging-stable` |
+| `production/*` | `production/app-server`, `production/load-balancer` |
+| `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` |
+
+Protected branch settings (like "Developers can push") apply to all matching
+branches.
+
+Two different wildcards can potentially match the same branch. For example,
+`*-stable` and `production-*` would both match a `production-stable` branch.
+In that case, if _any_ of these protected branches have a setting like
+"Allowed to push", then `production-stable` will also inherit this setting.
+
+If you click on a protected branch's name, you will be presented with a list of
+all matching branches:
+
+![Protected branch matches](img/protected_branches_matches.png)
+
+## Changelog
+
+**8.11**
+
+- Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081]
+
+**8.10**
+
+- Allow developers to merge into a protected branch without having push access [gitlab-org/gitlab-ce!4892][ce-4892]
+- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!4665][ce-4665]
+
+---
+
+[ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards"
+[ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access"
+[ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to"
+[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 38e9786123d..08ff89ce6ae 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -1,18 +1,18 @@
# Project import/export
>**Notes:**
- - This feature was [introduced][ce-3050] in GitLab 8.9
- - Importing will not be possible if the import instance version is lower
- than that of the exporter.
- - For existing installations, the project import option has to be enabled in
- application settings (`/admin/application_settings`) under 'Import sources'.
- Ask your administrator if you don't see the **GitLab export** button when
- creating a new project.
- - You can find some useful raketasks if you are an administrator in the
- [import_export](../../../administration/raketasks/project_import_export.md)
- raketask.
- - The exports are stored in a temporary [shared directory][tmp] and are deleted
- every 24 hours by a specific worker.
+>
+> - [Introduced][ce-3050] in GitLab 8.9.
+> - Importing will not be possible if the import instance version is lower
+> than that of the exporter.
+> - For existing installations, the project import option has to be enabled in
+> application settings (`/admin/application_settings`) under 'Import sources'.
+> You will have to be an administrator to enable and use the import functionality.
+> - You can find some useful raketasks if you are an administrator in the
+> [import_export](../../../administration/raketasks/project_import_export.md)
+> raketask.
+> - The exports are stored in a temporary [shared directory][tmp] and are deleted
+> every 24 hours by a specific worker.
Existing projects running on any GitLab instance or GitLab.com can be exported
with all their related data and be moved into a new GitLab instance.
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
new file mode 100644
index 00000000000..1792a0c501d
--- /dev/null
+++ b/doc/user/project/slash_commands.md
@@ -0,0 +1,30 @@
+# GitLab slash commands
+
+Slash commands are textual shortcuts for common actions on issues or merge
+requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
+You can enter these commands while creating a new issue or merge request, and
+in comments. Each command should be on a separate line in order to be properly
+detected and executed. The commands are removed from the issue, merge request or
+comment body before it is saved and will not be visible to anyone else.
+
+Below is a list of all of the available commands and descriptions about what they
+do.
+
+| Command | Action |
+|:---------------------------|:-------------|
+| `/close` | Close the issue or merge request |
+| `/reopen` | Reopen the issue or merge request |
+| `/title <New title>` | Change title |
+| `/assign @username` | Assign |
+| `/unassign` | Remove assignee |
+| `/milestone %milestone` | Set milestone |
+| `/remove_milestone` | Remove milestone |
+| `/label ~foo ~"bar baz"` | Add label(s) |
+| `/unlabel ~foo ~"bar baz"` | Remove all or specific label(s) |
+| `/relabel ~foo ~"bar baz"` | Replace all label(s) |
+| `/todo` | Add a todo |
+| `/done` | Mark todo as done |
+| `/subscribe` | Subscribe |
+| `/unsubscribe` | Unsubscribe |
+| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
+| `/remove_due_date` | Remove due date |
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 8559b67af04..33c1a79d59c 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -26,6 +26,10 @@ 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
+
+If you specify a secret token, it will be sent with the hook request in the `X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify that the request is legitimate.
+
## SSL Verification
By default, the SSL certificate of the webhook endpoint is verified based on
@@ -750,6 +754,174 @@ X-Gitlab-Event: Wiki Page Hook
}
```
+## Pipeline events
+
+Triggered on status change of Pipeline.
+
+**Request Header**:
+
+```
+X-Gitlab-Event: Pipeline Hook
+```
+
+**Request Body**:
+
+```json
+{
+ "object_kind": "pipeline",
+ "object_attributes":{
+ "id": 31,
+ "ref": "master",
+ "tag": false,
+ "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "status": "success",
+ "stages":[
+ "build",
+ "test",
+ "deploy"
+ ],
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "duration": 63
+ },
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "project":{
+ "name": "Gitlab Test",
+ "description": "Atque in sunt eos similique dolores voluptatem.",
+ "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test",
+ "avatar_url": null,
+ "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git",
+ "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git",
+ "namespace": "Gitlab Org",
+ "visibility_level": 20,
+ "path_with_namespace": "gitlab-org/gitlab-test",
+ "default_branch": "master"
+ },
+ "commit":{
+ "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "message": "test\n",
+ "timestamp": "2016-08-12T17:23:21+02:00",
+ "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2",
+ "author":{
+ "name": "User",
+ "email": "user@gitlab.com"
+ }
+ },
+ "builds":[
+ {
+ "id": 380,
+ "stage": "deploy",
+ "name": "production",
+ "status": "skipped",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "manual",
+ "manual": true,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 377,
+ "stage": "test",
+ "name": "test-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 378,
+ "stage": "test",
+ "name": "test-build",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:26:12 UTC",
+ "finished_at": "2016-08-12 15:26:29 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 376,
+ "stage": "build",
+ "name": "build-image",
+ "status": "success",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": "2016-08-12 15:24:56 UTC",
+ "finished_at": "2016-08-12 15:25:26 UTC",
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ },
+ {
+ "id": 379,
+ "stage": "deploy",
+ "name": "staging",
+ "status": "created",
+ "created_at": "2016-08-12 15:23:28 UTC",
+ "started_at": null,
+ "finished_at": null,
+ "when": "on_success",
+ "manual": false,
+ "user":{
+ "name": "Administrator",
+ "username": "root",
+ "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon"
+ },
+ "runner": null,
+ "artifacts_file":{
+ "filename": null,
+ "size": null
+ }
+ }
+ ]
+}
+```
+
#### Example webhook receiver
If you want to see GitLab's webhooks in action for testing purposes you can use
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index ddb2f7281b1..0cf56449de2 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -2,9 +2,11 @@
- [Authorization for merge requests](authorization_for_merge_requests.md)
- [Change your time zone](timezone.md)
+- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
+- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
- [File finder](file_finder.md)
- [Labels](../user/project/labels.md)
@@ -12,7 +14,8 @@
- [Project Features](project_features.md)
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
-- [Protected branches](protected_branches.md)
+- [Protected branches](../user/project/protected_branches.md)
+- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index 4b551130255..e541111d7b3 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -23,7 +23,7 @@ want to add.
---
-Select the user and the [permission level](../../permissions/permissions.md)
+Select the user and the [permission level](../../user/permissions.md)
that you'd like to give the user. Note that you can select more than one user.
![Give user permissions](img/add_user_give_permissions.png)
@@ -90,6 +90,9 @@ GitLab account using the same e-mail address the invitation was sent to.
## Request access to a project
+As a project owner you can enable or disable non members to request access to
+your project. Go to the project settings and click on **Allow users to request access**.
+
As a user, you can request to be a member of a project. Go to the project you'd
like to be a member of, and click the **Request Access** button on the right
side of your screen.
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index e6f8b792707..1df0698afd0 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,7 +1,7 @@
# Award emoji
>**Note:**
-This feature was [introduced][1825] in GitLab 8.2.
+[Introduced][1825] in GitLab 8.2.
When you're collaborating online, you get fewer opportunities for high-fives
and thumbs-ups. Emoji can be awarded to issues and merge requests, making
@@ -16,7 +16,7 @@ award emoji.
## Sort issues and merge requests on vote count
>**Note:**
-This feature was [introduced][2871] in GitLab 8.5.
+[Introduced][2871] in GitLab 8.5.
You can quickly sort issues and merge requests by the number of votes they
have received. The sort options can be found in the dropdown menu as "Most
@@ -45,7 +45,7 @@ downvotes.
## Award emoji for comments
>**Note:**
-This feature was [introduced][4291] in GitLab 8.9.
+[Introduced][4291] in GitLab 8.9.
Award emoji can also be applied to individual comments when you want to
celebrate an accomplishment or agree with an opinion.
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
index 4a499009842..64b94d81024 100644
--- a/doc/workflow/cherry_pick_changes.md
+++ b/doc/workflow/cherry_pick_changes.md
@@ -1,7 +1,6 @@
# Cherry-pick changes
->**Note:**
-This feature was [introduced][ce-3514] in GitLab 8.7.
+> [Introduced][ce-3514] in GitLab 8.7.
---
diff --git a/doc/workflow/file_finder.md b/doc/workflow/file_finder.md
index b69ae663272..8d87b030c83 100644
--- a/doc/workflow/file_finder.md
+++ b/doc/workflow/file_finder.md
@@ -1,6 +1,6 @@
# File finder
-_**Note:** This feature was [introduced][gh-9889] in GitLab 8.4._
+> [Introduced][gh-9889] in GitLab 8.4.
---
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
index 217a4a4012f..733d079bd4a 100644
--- a/doc/workflow/forking_workflow.md
+++ b/doc/workflow/forking_workflow.md
@@ -38,7 +38,7 @@ Forking a project is in most cases a two-step process.
---
After the forking is done, you can start working on the newly created
-repository. There, you will have full [Owner](../permissions/permissions.md)
+repository. There, you will have full [Owner](../user/permissions.md)
access, so you can set it up as you please.
## Merging upstream
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2b2f140f8bf..7c0eb90d540 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -89,7 +89,7 @@ In this case the master branch is deployed on staging. When someone wants to dep
And going live with code happens by merging the pre-production branch into the production branch.
This workflow where commits only flow downstream ensures that everything has been tested on all environments.
If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
-If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
+If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
@@ -115,7 +115,7 @@ In this flow it is not common to have a production branch (or git flow master br
Merge or pull requests are created in a git management application and ask an assigned person to merge two branches.
Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch.
-Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.
+Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee.
In this article we'll refer to them as merge requests.
If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team.
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 9b50286b179..a693cc3d0fd 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -53,6 +53,9 @@ If necessary, you can increase the access level of an individual user for a spec
## Requesting access to a group
+As a group owner you can enable or disable non members to request access to
+your group. Go to the group settings and click on **Allow users to request access**.
+
As a user, you can request to be a member of a group. Go to the group you'd
like to be a member of, and click the **Request Access** button on the right
side of your screen.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index a2b2a4b88f9..306caabf6e6 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -18,9 +18,6 @@ At its current state, GitHub importer can import:
With GitLab 8.7+, references to pull requests and issues are preserved.
-It is not yet possible to import your cross-repository pull requests (those from
-forks). We are working on improving this in the near future.
-
The importer page is visible when you [create a new project][new-project].
Click on the **GitHub** link and, if you are logged in via the GitHub
integration, you will be redirected to GitHub for permission to access your
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index d2ec56e6504..40a5e4476be 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -15,6 +15,25 @@ Please note that you need to have builds configured to enable this feature.
## Checkout merge requests locally
+### By adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any remote, e.g. to check out a merge request number 5 as shown in GitLab from the `upstream` remote, do:
+
+```
+$ git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check it out.
+
+### By modifying `.git/config` for a given repository
+
Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
```
@@ -34,7 +53,7 @@ It should look like this:
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
-Now you can fetch all the merge requests requests:
+Now you can fetch all the merge requests:
```
$ git fetch origin
@@ -61,3 +80,11 @@ If you click the "Hide whitespace changes" button, you can see the diff without
It is also working on commits compare view.
![Commit Compare](merge_requests/commit_compare.png)
+
+## Merge Requests versions
+
+Every time you push to merge request branch, a new version of merge request diff
+is created. When you visit the merge request page you see latest version of changes.
+However you can select an older one from version dropdown
+
+![Merge Request Versions](merge_requests/versions.png)
diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png
new file mode 100644
index 00000000000..c0a6dfe6806
--- /dev/null
+++ b/doc/workflow/merge_requests/versions.png
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index b4a9c2f3d3e..1b49a5c385f 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -67,7 +67,7 @@ In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- authors of comments on the issue/merge request
- - anyone mentioned by `@username` in the issue/merge request description
+ - 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
...with notification level "Participating" or higher
@@ -89,6 +89,11 @@ In all of the below cases, the notification will be sent to:
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
+
+In addition, if the title or description of an Issue or Merge Request is
+changed, notifications will be sent to any **new** mentions by `@username` as
+if they had been mentioned in the original text.
+
You won't receive notifications for Issues, Merge Requests or Milestones
created by yourself. You will only receive automatic notifications when
somebody else comments or adds changes to the ones that you've created or
diff --git a/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index 67adfc2f43a..aa48b8f750e 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -1,55 +1 @@
-# Protected Branches
-
-Permissions in GitLab are fundamentally defined around the idea of having read or write permission to the repository and branches.
-
-To prevent people from messing with history or pushing code without review, we've created protected branches.
-
-A protected branch does three simple things:
-
-* it prevents pushes from everybody except users with Master permission
-* it prevents anyone from force pushing to the branch
-* it prevents anyone from deleting the branch
-
-You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
-
-To protect a branch, user needs to have at least a Master permission level, see [permissions document](../permissions/permissions.md).
-
-![protected branches page](protected_branches/protected_branches1.png)
-
-Navigate to project settings page and select `protected branches`. From the `Branch` dropdown menu select the branch you want to protect.
-
-Some workflows, like [GitLab workflow](gitlab_flow.md), require all users with write access to submit a Merge request in order to get the code into a protected branch.
-
-Since Masters and Owners can already push to protected branches, that means Developers cannot push to protected branch and need to submit a Merge request.
-
-However, there are workflows where that is not needed and only protecting from force pushes and branch removal is useful.
-
-For those workflows, you can allow everyone with write access to push to a protected branch by selecting `Developers can push` check box.
-
-On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box.
-
-![Developers can push](protected_branches/protected_branches2.png)
-
-## Wildcard Protected Branches
-
->**Note:**
-This feature was added in GitLab 8.10.
-
-1. You can specify a wildcard protected branch, which will protect all branches matching the wildcard. For example:
-
- | Wildcard Protected Branch | Matching Branches |
- |---------------------------+--------------------------------------------------------|
- | `*-stable` | `production-stable`, `staging-stable` |
- | `production/*` | `production/app-server`, `production/load-balancer` |
- | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` |
-
-1. Protected branch settings (like "Developers Can Push") apply to all matching branches.
-
-1. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch.
- >**Note:**
- If _any_ of these protected branches have "Developers Can Push" set to true, then `production-stable` has it set to true.
-
-1. If you click on a protected branch's name, you will be presented with a list of all matching branches:
-
- ![protected branch matches](protected_branches/protected_branches3.png)
-
+This document is moved to [user/project/protected_branches.md](../user/project/protected_branches.md)
diff --git a/doc/workflow/protected_branches/protected_branches1.png b/doc/workflow/protected_branches/protected_branches1.png
deleted file mode 100644
index c00443803de..00000000000
--- a/doc/workflow/protected_branches/protected_branches1.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/protected_branches/protected_branches2.png b/doc/workflow/protected_branches/protected_branches2.png
deleted file mode 100644
index a4f664d3b21..00000000000
--- a/doc/workflow/protected_branches/protected_branches2.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/protected_branches/protected_branches3.png b/doc/workflow/protected_branches/protected_branches3.png
deleted file mode 100644
index 2a50cb174bb..00000000000
--- a/doc/workflow/protected_branches/protected_branches3.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
index 399366b0cdc..5ead9f4177f 100644
--- a/doc/workflow/revert_changes.md
+++ b/doc/workflow/revert_changes.md
@@ -1,6 +1,6 @@
# Reverting changes
-_**Note:** This feature was [introduced][ce-1990] in GitLab 8.5._
+> [Introduced][ce-1990] in GitLab 8.5.
---
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 4c59f59c587..8e50cb03e63 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,22 +1,24 @@
# Share Projects with other Groups
-In GitLab Enterprise Edition you can share projects with other groups.
-This makes it possible to add a group of users to a project with a single action.
+You can share projects with other groups. This makes it possible to add a group of users
+to a project with a single action.
## Groups as collections of users
-In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
-In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+Groups are used primarily to [create collections of projects](groups.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
## Sharing a project with a group of users
-The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
-But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the (Enterprise Edition only) group sharing feature can be of use.
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
-![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
diff --git a/doc/workflow/shortcuts.md b/doc/workflow/shortcuts.md
index ffcb832cdd7..36516883ef6 100644
--- a/doc/workflow/shortcuts.md
+++ b/doc/workflow/shortcuts.md
@@ -2,4 +2,75 @@
You can see GitLab's keyboard shortcuts by using 'shift + ?'
-![Shortcuts](shortcuts.png) \ No newline at end of file
+## Global Shortcuts
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>s</kbd> | Focus search |
+| <kbd>?</kbd> | Show/hide this dialog |
+| <kbd>⌘</kbd> + <kbd>shift</kbd> + <kbd>p</kbd> | Toggle markdown preview |
+| <kbd>↑</kbd> | Edit last comment (when focused on an empty textarea) |
+
+## Project Files Browsing
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>↑</kbd> | Move selection up |
+| <kbd>↓</kbd> | Move selection down |
+| <kbd>enter</kbd> | Open selection |
+
+## Finding Project File
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>↑</kbd> | Move selection up |
+| <kbd>↓</kbd> | Move selection down |
+| <kbd>enter</kbd> | Open selection |
+| <kbd>esc</kbd> | Go back |
+
+## Global Dashboard
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>g</kbd> + <kbd>a</kbd> | Go to the activity feed |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to projects |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+
+## Project
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>g</kbd> + <kbd>p</kbd> | Go to the project's home page |
+| <kbd>g</kbd> + <kbd>e</kbd> | Go to the project's activity feed |
+| <kbd>g</kbd> + <kbd>f</kbd> | Go to files |
+| <kbd>g</kbd> + <kbd>c</kbd> | Go to commits |
+| <kbd>g</kbd> + <kbd>b</kbd> | Go to builds |
+| <kbd>g</kbd> + <kbd>n</kbd> | Go to network graph |
+| <kbd>g</kbd> + <kbd>g</kbd> | Go to graphs |
+| <kbd>g</kbd> + <kbd>i</kbd> | Go to issues |
+| <kbd>g</kbd> + <kbd>m</kbd> | Go to merge requests |
+| <kbd>g</kbd> + <kbd>s</kbd> | Go to snippets |
+| <kbd>t</kbd> | Go to finding file |
+| <kbd>i</kbd> | New issue |
+
+## Network Graph
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>←</kbd> or <kbd>h</kbd> | Scroll left |
+| <kbd>→</kbd> or <kbd>l</kbd> | Scroll right |
+| <kbd>↑</kbd> or <kbd>k</kbd> | Scroll up |
+| <kbd>↓</kbd> or <kbd>j</kbd> | Scroll down |
+| <kbd>shift</kbd> + <kbd>↑</kbd> or <kbd>shift</kbd> + <kbd>k</kbd> | Scroll to top |
+| <kbd>shift</kbd> + <kbd>↓</kbd> or <kbd>shift</kbd> + <kbd>j</kbd> | Scroll to bottom |
+
+## Issues and Merge Requests
+
+| Keyboard Shortcut | Description |
+| ----------------- | ----------- |
+| <kbd>a</kbd> | Change assignee |
+| <kbd>m</kbd> | Change milestone |
+| <kbd>r</kbd> | Reply (quoting selected text) |
+| <kbd>e</kbd> | Edit issue/merge request |
+| <kbd>l</kbd> | Change label | \ No newline at end of file
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
deleted file mode 100644
index a9b1c4b4dcc..00000000000
--- a/doc/workflow/shortcuts.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 9524ffd5420..a50ba305deb 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -1,6 +1,6 @@
# GitLab Todos
->**Note:** This feature was [introduced][ce-2817] in GitLab 8.5.
+> [Introduced][ce-2817] in GitLab 8.5.
When you log into GitLab, you normally want to see where you should spend your
time and take some action, or what you need to keep an eye on. All without the
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index 1832567a34c..ee8e7862572 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -70,8 +70,7 @@ There are multiple ways to create a branch from GitLab's web interface.
### Create a new branch from an issue
->**Note:**
-This feature was [introduced][ce-2808] in GitLab 8.6.
+> [Introduced][ce-2808] in GitLab 8.6.
In case your development workflow dictates to have an issue for every merge
request, you can quickly create a branch right on the issue page which will be
diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature
index 8ddafb6a7ac..046e2815d4e 100644
--- a/features/dashboard/new_project.feature
+++ b/features/dashboard/new_project.feature
@@ -9,7 +9,7 @@ Background:
@javascript
Scenario: I should see New Projects page
Then I see "New Project" page
- Then I see all possible import optios
+ Then I see all possible import options
@javascript
Scenario: I should see instructions on how to import from Git URL
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index 5fc9b135601..9eacbe0b25e 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -24,14 +24,6 @@ Feature: Explore Groups
Then I should see project "Internal" items
And I should not see project "Enterprise" items
- Scenario: I should see group's members as user
- Given group "TestGroup" has internal project "Internal"
- And "John Doe" is owner of group "TestGroup"
- When I sign in as a user
- And I visit group "TestGroup" members page
- Then I should see group member "John Doe"
- And I should not see member roles
-
Scenario: I should see group with private, internal and public projects as visitor
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -56,14 +48,6 @@ Feature: Explore Groups
And I should not see project "Internal" items
And I should not see project "Enterprise" items
- Scenario: I should see group's members as visitor
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- And "John Doe" is owner of group "TestGroup"
- When I visit group "TestGroup" members page
- Then I should see group member "John Doe"
- And I should not see member roles
-
Scenario: I should see group with private, internal and public projects as user
Given group "TestGroup" has internal project "Internal"
Given group "TestGroup" has public project "Community"
@@ -91,15 +75,6 @@ Feature: Explore Groups
And I should see project "Internal" items
And I should not see project "Enterprise" items
- Scenario: I should see group's members as user
- Given group "TestGroup" has internal project "Internal"
- Given group "TestGroup" has public project "Community"
- And "John Doe" is owner of group "TestGroup"
- When I sign in as a user
- And I visit group "TestGroup" members page
- Then I should see group member "John Doe"
- And I should not see member roles
-
Scenario: I should see group with public project in public groups area
Given group "TestGroup" has public project "Community"
When I visit the public groups area
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 2c17d32154a..88fef674c0c 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -22,6 +22,7 @@ Feature: Project Commits Branches
@javascript
Scenario: I delete a branch
Given I visit project branches page
+ And I filter for branch improve/awesome
And I click branch 'improve/awesome' delete link
Then I should not see branch 'improve/awesome'
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index a95df038357..8b0cb90765e 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -83,11 +83,6 @@ Feature: Project Commits
#Given I visit my project's commits stats page
#Then I see commits stats
- Scenario: I browse big commit
- Given I visit big commit page
- Then I see big commit warning
- And I see "Reload with full diff" link
-
Scenario: I browse a commit with an image
Given I visit a commit with an image that changed
Then The diff links to both the previous and current image
diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature
index 2bde4c8a99b..35687aac9ea 100644
--- a/features/project/commits/diff_comments.feature
+++ b/features/project/commits/diff_comments.feature
@@ -6,10 +6,6 @@ Feature: Project Commits Diff Comments
And I visit project commit page
@javascript
- Scenario: I can access add diff comment buttons
- Then I should see add a diff comment button
-
- @javascript
Scenario: I can comment on a commit diff
Given I leave a diff comment like "Typo, please fix"
Then I should see a diff comment saying "Typo, please fix"
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 21768c15c17..5aa592e9067 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -24,7 +24,7 @@ Feature: Project Merge Requests
Scenario: I should see target branch when it is different from default
Given project "Shop" have "Bug NS-06" open merge request
When I visit project "Shop" merge requests page
- Then I should see "other_branch" branch
+ Then I should see "feature_conflict" branch
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
@@ -89,7 +89,7 @@ Feature: Project Merge Requests
Then The list should be sorted by "Oldest updated"
@javascript
- Scenario: Visiting Merge Requests from a differente Project after sorting
+ Scenario: Visiting Merge Requests from a different Project after sorting
Given I visit project "Shop" merge requests page
And I sort the list by "Oldest updated"
And I visit dashboard merge requests page
@@ -237,6 +237,15 @@ Feature: Project Merge Requests
Then I should see additional file lines
@javascript
+ Scenario: I unfold diff in Side-by-Side view
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And I visit merge request page "Bug NS-05"
+ And I click on the Changes tab
+ And I click Side-by-side Diff tab
+ And I unfold diff
+ Then I should see additional file lines
+
+ @javascript
Scenario: I show comments on a merge request side-by-side diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index d4811b1ff54..63ce3ccb536 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -8,6 +8,12 @@ Feature: Project Wiki
Given I create the Wiki Home page
Then I should see the newly created wiki page
+ Scenario: Add new page with errors
+ Given I create the Wiki Home page with no content
+ Then I should see a "Content can't be blank" error message
+ When I create the Wiki Home page
+ Then I should see the newly created wiki page
+
Scenario: Pressing Cancel while editing a brand new Wiki
Given I click on the Cancel button
Then I should be redirected back to the Edit Home Wiki page
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 037f7494a77..03f87df7a60 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -27,19 +27,19 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
step 'I check all events and submit form' do
page.check('Active')
- page.check('Push events')
- page.check('Tag push events')
- page.check('Comments')
- page.check('Issues events')
- page.check('Merge Request events')
- page.check('Build events')
+ page.check('Push')
+ page.check('Tag push')
+ page.check('Note')
+ page.check('Issue')
+ page.check('Merge request')
+ page.check('Build')
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 'Channel', with: '#test_channel'
+ fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken builds')
end
@@ -56,6 +56,6 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
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_field('Channel').value).to eq '#test_channel'
+ expect(find('#service_push_channel').value).to eq '#test_channel'
end
end
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index 80ed4c6d64c..a7d61bc28e0 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -26,6 +26,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I see prefilled new Merge Request page' do
+ expect(page).to have_selector('.merge-request-form')
expect(current_path).to eq new_namespace_project_merge_request_path(@project.namespace, @project)
expect(find("#merge_request_target_project_id").value).to eq @project.id.to_s
expect(find("input#merge_request_source_branch").value).to eq "fix"
diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb
index 726b37cfde5..ca3cd0ecc4e 100644
--- a/features/steps/dashboard/event_filters.rb
+++ b/features/steps/dashboard/event_filters.rb
@@ -1,4 +1,5 @@
class Spinach::Features::EventFilters < Spinach::FeatureSteps
+ include WaitForAjax
include SharedAuthentication
include SharedPaths
include SharedProject
@@ -72,14 +73,20 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps
end
When 'I click "push" event filter' do
- click_link("push_event_filter")
+ wait_for_ajax
+ click_link("Push events")
+ wait_for_ajax
end
When 'I click "team" event filter' do
- click_link("team_event_filter")
+ wait_for_ajax
+ click_link("Team")
+ wait_for_ajax
end
When 'I click "merge" event filter' do
- click_link("merged_event_filter")
+ wait_for_ajax
+ click_link("Merge events")
+ wait_for_ajax
end
end
diff --git a/features/steps/dashboard/help.rb b/features/steps/dashboard/help.rb
index 800e869533e..9c94dc70df0 100644
--- a/features/steps/dashboard/help.rb
+++ b/features/steps/dashboard/help.rb
@@ -8,7 +8,7 @@ class Spinach::Features::DashboardHelp < Spinach::FeatureSteps
end
step 'I visit the "Rake Tasks" help page' do
- visit help_page_path("raketasks", "maintenance")
+ visit help_page_path("raketasks/maintenance")
end
step 'I should see "Rake Tasks" page markdown rendered' do
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index 8706f0e8e78..39c65bb6cde 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -43,9 +43,14 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
step 'I click "All" link' do
find(".js-author-search").click
+ expect(page).to have_selector(".dropdown-menu-author li a")
find(".dropdown-menu-author li a", match: :first).click
+ expect(page).not_to have_selector(".dropdown-menu-author li a")
+
find(".js-assignee-search").click
+ expect(page).to have_selector(".dropdown-menu-assignee li a")
find(".dropdown-menu-assignee li a", match: :first).click
+ expect(page).not_to have_selector(".dropdown-menu-assignee li a")
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 06db36c7014..6777101fb15 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -47,9 +47,14 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
step 'I click "All" link' do
find(".js-author-search").click
+ expect(page).to have_selector(".dropdown-menu-author li a")
find(".dropdown-menu-author li a", match: :first).click
+ expect(page).not_to have_selector(".dropdown-menu-author li a")
+
find(".js-assignee-search").click
+ expect(page).to have_selector(".dropdown-menu-assignee li a")
find(".dropdown-menu-assignee li a", match: :first).click
+ expect(page).not_to have_selector(".dropdown-menu-assignee li a")
end
def should_see(merge_request)
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 727a6a71373..2f0941e4113 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -14,14 +14,12 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_content('Project name')
end
- step 'I see all possible import optios' do
+ step 'I see all possible import options' do
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
- expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
- expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
@@ -29,6 +27,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
end
step 'I am redirected to the GitHub import page' do
+ expect(page).to have_content('Import Projects from GitHub')
expect(current_path).to eq new_import_github_path
end
@@ -47,6 +46,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
end
step 'I redirected to Google Code import page' do
+ expect(page).to have_content('Import projects from Google Code')
expect(current_path).to eq new_import_google_code_path
end
end
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
index 87f32e70d59..409bf0cb416 100644
--- a/features/steps/explore/groups.rb
+++ b/features/steps/explore/groups.rb
@@ -62,10 +62,6 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
expect(page).to have_content "John Doe"
end
- step 'I should not see member roles' do
- expect(body).not_to match(%r{owner|developer|reporter|guest}i)
- end
-
protected
def group_has_project(groupname, projectname, visibility_level)
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def..e9b45823c67 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- click_button "Edit access level"
- select 'Developer', from: 'group_member_access_level'
+ click_button 'Edit'
+ select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save'
end
end
diff --git a/features/steps/project/badges/build.rb b/features/steps/project/badges/build.rb
index 66a48a176e5..96c59322f9b 100644
--- a/features/steps/project/badges/build.rb
+++ b/features/steps/project/badges/build.rb
@@ -26,7 +26,7 @@ class Spinach::Features::ProjectBadgesBuild < Spinach::FeatureSteps
def expect_badge(status)
svg = Nokogiri::XML.parse(page.body)
- expect(page.response_headers).to include('Content-Type' => 'image/svg+xml')
+ expect(page.response_headers['Content-Type']).to include('image/svg+xml')
expect(svg.at(%Q{text:contains("#{status}")})).to be_truthy
end
end
diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb
index 2876e8812e9..055fca036d3 100644
--- a/features/steps/project/builds/artifacts.rb
+++ b/features/steps/project/builds/artifacts.rb
@@ -10,6 +10,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
step 'I click artifacts browse button' do
click_link 'Browse'
+ expect(page).not_to have_selector('.build-sidebar')
end
step 'I should see content of artifacts archive' do
@@ -68,10 +69,16 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps
end
step 'download of a file extracted from build artifacts should start' do
- # this will be accelerated by Workhorse
- response_json = JSON.parse(page.body, symbolize_names: true)
- expect(response_json[:archive]).to end_with('build_artifacts.zip')
- expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt')
+ send_data = response_headers[Gitlab::Workhorse::SEND_DATA_HEADER]
+
+ expect(send_data).to start_with('artifacts-entry:')
+
+ base64_params = send_data.sub(/\Aartifacts\-entry:/, '')
+ params = JSON.parse(Base64.urlsafe_decode64(base64_params))
+
+ expect(params.keys).to eq(['Archive', 'Entry'])
+ expect(params['Archive']).to end_with('build_artifacts.zip')
+ expect(params['Entry']).to eq(Base64.encode64('ci_artifacts.txt'))
end
step 'I click a first row within build artifacts table' do
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 0a42931147d..5f9b9e0445e 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -25,7 +25,7 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
step 'project "Shop" has protected branches' do
project = Project.find_by(name: "Shop")
- project.protected_branches.create(name: "stable")
+ create(:protected_branch, project: project, name: "stable")
end
step 'I click new branch link' do
@@ -73,6 +73,11 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content 'Branch already exists'
end
+ step 'I filter for branch improve/awesome' do
+ fill_in 'branch-search', with: 'improve/awesome'
+ find('#branch-search').native.send_keys(:enter)
+ end
+
step "I click branch 'improve/awesome' delete link" do
page.within '.js-branch-improve\/awesome' do
find('.btn-remove').click
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 239036e431d..bea9f9d198b 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -125,25 +125,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content 'Authors'
end
- step 'I visit big commit page' do
- # Create a temporary scope to ensure that the stub_const is removed after user
- RSpec::Mocks.with_temporary_scope do
- stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_lines: 1, max_files: 1 })
- visit namespace_project_commit_path(@project.namespace, @project, sample_big_commit.id)
- end
- end
-
- step 'I see big commit warning' do
- expect(page).to have_content sample_big_commit.message
- expect(page).to have_content "Too many changes"
- end
-
- step 'I see "Reload with full diff" link' do
- link = find_link('Reload with full diff')
- expect(link[:href]).to end_with('?force_show_diff=true')
- expect(link[:href]).not_to include('.html')
- end
-
step 'I visit a commit with an image that changed' do
visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id)
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 6b56a77b832..dacab6c7977 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -34,6 +34,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I fill out a "Merge Request On Forked Project" merge request' do
+ expect(page).to have_content('Source branch')
+ expect(page).to have_content('Target branch')
+
first('.js-source-project').click
first('.dropdown-source-project a', text: @forked_project.path_with_namespace)
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1498f899cf5..cbe5738e7e4 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -48,7 +48,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do
expect(page).to have_selector '.js-emoji-btn'
expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1'
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='me']")
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 35f166c7c08..e21f76d00d9 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -45,6 +45,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I click link "All"' do
click_link "All"
+ # Waits for load
+ expect(find('.issues-state-filters > .active')).to have_content 'All'
end
step 'I click link "Release 0.4"' do
@@ -297,7 +299,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I fill in issue search with \'Rock and roll\'' do
- filter_issue 'Description for issue'
+ filter_issue 'Rock and roll'
end
step 'I should see \'Bugfix1\' in issues' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index da848afd48e..56b28949585 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -22,6 +22,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click link "All"' do
click_link "All"
+ # Waits for load
+ expect(find('.issues-state-filters > .active')).to have_content 'All'
end
step 'I click link "Merged"' do
@@ -56,8 +58,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(find('.merge-request-info')).not_to have_content "master"
end
- step 'I should see "other_branch" branch' do
- expect(page).to have_content "other_branch"
+ step 'I should see "feature_conflict" branch' do
+ expect(page).to have_content "feature_conflict"
end
step 'I should see "Bug NS-04" in merge requests' do
@@ -122,7 +124,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
source_project: project,
target_project: project,
source_branch: 'fix',
- target_branch: 'other_branch',
+ target_branch: 'feature_conflict',
author: project.users.first,
description: "# Description header"
)
@@ -477,6 +479,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click Side-by-side Diff tab' do
find('a', text: 'Side-by-side').trigger('click')
+
+ # Waits for load
+ expect(page).to have_css('.parallel')
end
step 'I should see comments on the side-by-side diff page' do
@@ -490,6 +495,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click the "Target branch" dropdown' do
+ expect(page).to have_content('Target branch')
first('.target_branch').click
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 0fe046dcbf6..bb79424ee08 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content with new lines preserved at end of file' do
- expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
end
step 'I click link "Raw"' do
@@ -65,15 +65,16 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I can edit code' do
set_new_content
- expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
end
step 'I edit code' do
+ expect(page).to have_selector('.file-editor')
set_new_content
end
step 'I edit code with new lines at end of file' do
- execute_script('blob.editor.setValue("Sample\n\n\n")')
+ execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
end
step 'I fill the new file name' do
@@ -131,6 +132,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I click on "New file" link in repo' do
find('.add-to-tree').click
click_link 'New file'
+ expect(page).to have_selector('.file-editor')
end
step 'I click on "Upload file" link in repo' do
@@ -293,7 +295,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
first('.js-project-refs-dropdown').click
page.within '.project-refs-form' do
- click_link 'test'
+ click_link "'test'"
end
end
@@ -376,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
- execute_script("blob.editor.setValue('#{new_gitignore_content}')")
+ execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
# Content of the gitignore file on the seed repository.
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1..e920f5a706b 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,8 +65,8 @@ 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
- click_button "Edit access level"
- select "Reporter", from: "project_member_access_level"
+ click_button 'Edit'
+ select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 3cbf832c728..07a955b1a14 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -19,6 +19,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
click_on "Create page"
end
+ step 'I create the Wiki Home page with no content' do
+ fill_in "wiki_content", with: ''
+ click_on "Create page"
+ end
+
step 'I should see the newly created wiki page' do
expect(page).to have_content "Home"
expect(page).to have_content "link test"
@@ -125,24 +130,26 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I create a New page with paths' do
click_on 'New Page'
- fill_in 'Page slug', with: 'one/two/three'
+ fill_in 'Page slug', with: 'one/two/three-test'
click_on 'Create Page'
fill_in "wiki_content", with: 'wiki content'
click_on "Create page"
- expect(current_path).to include 'one/two/three'
+ expect(current_path).to include 'one/two/three-test'
end
step 'I should see non-escaped link in the pages list' do
- expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
+ expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three-test']")
end
step 'I edit the Wiki page with a path' do
+ expect(page).to have_content('three')
click_on 'three'
+ expect(find('.nav-text')).to have_content('Three')
click_on 'Edit'
end
step 'I should see a non-escaped path' do
- expect(current_path).to include 'one/two/three'
+ expect(current_path).to include 'one/two/three-test'
end
step 'I should see the Editing page' do
@@ -173,6 +180,11 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
find('a[href*="?version_id"]')
end
+ step 'I should see a "Content can\'t be blank" error message' do
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content('Content can\'t be blank')
+ end
+
def wiki
@project_wiki = ProjectWiki.new(project, current_user)
end
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
index 4d6b258f577..70e6d4836b2 100644
--- a/features/steps/shared/builds.rb
+++ b/features/steps/shared/builds.rb
@@ -10,20 +10,20 @@ module SharedBuilds
end
step 'project has a recent build' do
- @pipeline = create(:ci_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
+ @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master')
@build = create(:ci_build_with_coverage, pipeline: @pipeline)
end
step 'recent build is successful' do
- @build.update(status: 'success')
+ @build.success
end
step 'recent build failed' do
- @build.update(status: 'failed')
+ @build.drop
end
step 'project has another build that is running' do
- create(:ci_build, pipeline: @pipeline, name: 'second build', status: 'running')
+ create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run')
end
step 'I visit recent build details page' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 56ef44ec969..4df4e89f5b9 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -25,17 +25,16 @@ module SharedDiffNote
page.within("form[data-line-code='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").trigger("click")
- sleep 0.05
+ find(".js-comment-button").click
end
end
end
step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do
- click_parallel_diff_line(sample_commit.line_code, 'old')
- page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do
+ click_parallel_diff_line(sample_commit.del_line_code, 'old')
+ page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do
fill_in "note[note]", with: "Old comment"
- find(".js-comment-button").trigger("click")
+ find(".js-comment-button").click
end
end
@@ -43,7 +42,7 @@ module SharedDiffNote
click_parallel_diff_line(sample_commit.line_code, 'new')
page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do
fill_in "note[note]", with: "New comment"
- find(".js-comment-button").trigger("click")
+ find(".js-comment-button").click
end
end
@@ -165,10 +164,6 @@ module SharedDiffNote
end
end
- step 'I should see add a diff comment button' do
- expect(page).to have_css('.js-add-diff-note-button')
- end
-
step 'I should see an empty diff comment form' do
page.within(diff_file_selector) do
expect(page).to have_field("note[note]", with: "")
@@ -215,7 +210,7 @@ module SharedDiffNote
end
step 'I click side-by-side diff button' do
- find('#parallel-diff-btn').trigger('click')
+ find('#parallel-diff-btn').click
end
step 'I see side-by-side diff button' do
@@ -227,10 +222,12 @@ module SharedDiffNote
end
def click_diff_line(code)
- find("button[data-line-code='#{code}']").trigger('click')
+ find(".line_holder[id='#{code}'] td:nth-of-type(1)").trigger 'mouseover'
+ find(".line_holder[id='#{code}'] button").trigger 'click'
end
def click_parallel_diff_line(code, line_type)
- find("button[data-line-code='#{code}'][data-line-type='#{line_type}']").trigger('click')
+ find(".line_content.parallel.#{line_type}[data-line-code='#{code}']").trigger 'mouseover'
+ find(".line_holder.parallel button[data-line-code='#{code}']").trigger 'click'
end
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index b5fd24d246f..df9845ba569 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -133,9 +133,7 @@ module SharedIssuable
end
step 'The list should be sorted by "Oldest updated"' do
- page.within('.content div.dropdown.inline.prepend-left-10') do
- expect(page.find('button.dropdown-toggle.btn')).to have_content('Oldest updated')
- end
+ expect(find('.issues-filters')).to have_content('Oldest updated')
end
step 'I click link "Next" in the sidebar' do
@@ -181,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/support/env.rb b/features/support/env.rb
index ab3f0ca7aeb..569fd444e86 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -1,6 +1,5 @@
-if ENV['SIMPLECOV']
- require 'simplecov'
-end
+require './spec/simplecov_env'
+SimpleCovEnv.start!
ENV['RAILS_ENV'] = 'test'
require './config/environment'
@@ -13,7 +12,7 @@ require_relative 'rerun'
if ENV['CI']
require 'knapsack'
- Knapsack::Adapters::RSpecAdapter.bind
+ Knapsack::Adapters::SpinachAdapter.bind
end
%w(select2_helper test_env repo_helpers).each do |f|
diff --git a/features/support/wait_for_ajax.rb b/features/support/wait_for_ajax.rb
new file mode 100644
index 00000000000..b90fc112671
--- /dev/null
+++ b/features/support/wait_for_ajax.rb
@@ -0,0 +1,11 @@
+module WaitForAjax
+ def wait_for_ajax
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until finished_all_ajax_requests?
+ end
+ end
+
+ def finished_all_ajax_requests?
+ page.evaluate_script('jQuery.active').zero?
+ end
+end
diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json
index d3831d8045b..e2f47db0de2 100644
--- a/fixtures/emojis/aliases.json
+++ b/fixtures/emojis/aliases.json
@@ -1,16 +1,9 @@
{
- "northeast_pointing_airplane":"airplane_northeast",
"small_airplane":"airplane_small",
- "up_pointing_small_airplane":"airplane_small_up",
- "up_pointing_airplane":"airplane_up",
- "left_anger_bubble":"anger_left",
"right_anger_bubble":"anger_right",
"keycap_asterisk":"asterisk",
"atom_symbol":"atom",
"ballot_box_with_ballot":"ballot_box",
- "ballot_box_with_bold_check":"ballot_box_check",
- "ballot_box_with_script_x":"ballot_box_x",
- "ballot_script_x":"ballot_x",
"person_with_ball":"basketball_player",
"person_with_ball_tone1":"basketball_player_tone1",
"person_with_ball_tone2":"basketball_player_tone2",
@@ -21,51 +14,65 @@
"umbrella_on_ground":"beach_umbrella",
"bellhop_bell":"bellhop",
"biohazard_sign":"biohazard",
- "bouquet_of_flowers":"bouquet2",
"archery":"bow_and_arrow",
- "bullhorn_with_sound_waves":"bullhorn_waves",
- "pocket calculator":"calculator",
+ "boxing_gloves":"boxing_glove",
"spiral_calendar_pad":"calendar_spiral",
+ "call_me_hand":"call_me",
+ "call_me_hand_tone1":"call_me_tone1",
+ "call_me_hand_tone2":"call_me_tone2",
+ "call_me_hand_tone3":"call_me_tone3",
+ "call_me_hand_tone4":"call_me_tone4",
+ "call_me_hand_tone5":"call_me_tone5",
+ "kayak":"canoe",
"card_file_box":"card_box",
- "tape_cartridge":"cartridge",
+ "person_doing_cartwheel":"cartwheel",
+ "person_doing_cartwheel_tone1":"cartwheel_tone1",
+ "person_doing_cartwheel_tone2":"cartwheel_tone2",
+ "person_doing_cartwheel_tone3":"cartwheel_tone3",
+ "person_doing_cartwheel_tone4":"cartwheel_tone4",
+ "person_doing_cartwheel_tone5":"cartwheel_tone5",
"bottle_with_popping_cork":"champagne",
+ "clinking_glass":"champagne_glass",
"cheese_wedge":"cheese",
"city_sunrise":"city_sunset",
"mantlepiece_clock":"clock",
- "clockwise_right_and_left_semicircle_arrows":"clockwise_arrows",
"cloud_with_lightning":"cloud_lightning",
"cloud_with_rain":"cloud_rain",
"cloud_with_snow":"cloud_snow",
"cloud_with_tornado":"cloud_tornado",
- "old_personal_computer":"computer_old",
- "building_construction":"contruction_site",
+ "clown_face":"clown",
+ "building_construction":"construction_site",
"couch_and_lamp":"couch",
"couple_with_heart_mm":"couple_mm",
"couple_with_heart_ww":"couple_ww",
+ "face_with_cowboy_hat":"cowboy",
"lower_left_crayon":"crayon",
"cricket_bat_ball":"cricket",
"latin_cross":"cross",
- "heavy_latin_cross":"cross_heavy",
- "white_latin_cross":"cross_white",
- "black_skull_and_crossbones":"crossbones",
"passenger_ship":"cruise_ship",
"dagger_knife":"dagger",
"desktop_computer":"desktop",
"card_index_dividers":"dividers",
- "document_with_text":"document_text",
"dove_of_peace":"dove",
+ "drool":"drooling_face",
+ "drum_with_drumsticks":"drum",
"email":"e-mail",
- "back_of_envelope":"envelope_back",
- "flying_envelope":"envelope_flying",
- "stamped_envelope":"envelope_stamped",
- "pen_over_stamped_envelope":"envelope_stamped_pen",
- "white_down_pointing_left_hand_index":"finger_pointing_down",
- "sideways_white_down_pointing_index":"finger_pointing_down2",
- "sideways_white_left_pointing_index":"finger_pointing_left",
- "sideways_white_right_pointing_index":"finger_pointing_right",
- "sideways_white_up_pointing_index":"finger_pointing_up",
+ "eject_symbol":"eject",
+ "facepalm":"face_palm",
+ "facepalm_tone1":"face_palm_tone1",
+ "facepalm_tone2":"face_palm_tone2",
+ "facepalm_tone3":"face_palm_tone3",
+ "facepalm_tone4":"face_palm_tone4",
+ "facepalm_tone5":"face_palm_tone5",
+ "fencing":"fencer",
+ "hand_with_index_and_middle_finger_crossed":"fingers_crossed",
+ "hand_with_index_and_middle_fingers_crossed_tone1":"fingers_crossed_tone1",
+ "hand_with_index_and_middle_fingers_crossed_tone2":"fingers_crossed_tone2",
+ "hand_with_index_and_middle_fingers_crossed_tone3":"fingers_crossed_tone3",
+ "hand_with_index_and_middle_fingers_crossed_tone4":"fingers_crossed_tone4",
+ "hand_with_index_and_middle_fingers_crossed_tone5":"fingers_crossed_tone5",
"flame":"fire",
- "oncoming_fire_engine":"fire_engine_oncoming",
+ "first_place_medal":"first_place",
"ac":"flag_ac",
"ad":"flag_ad",
"ae":"flag_ae",
@@ -326,44 +333,51 @@
"za":"flag_za",
"zm":"flag_zm",
"zw":"flag_zw",
- "clamshell_mobile_phone":"flip_phone",
- "black_hard_shell_floppy_disk":"floppy_black",
- "white_hard_shell_floppy_disk":"floppy_white",
- "open_folder":"folder_open",
"fork_and_knife_with_plate":"fork_knife_plate",
+ "fox_face":"fox",
"frame_with_picture":"frame_photo",
- "frame_with_tiles":"frame_tiles",
- "frame_with_an_x":"frame_x",
+ "baguette_bread":"french_bread",
"anguished":"frowning",
"white_frowning_face":"frowning2",
+ "goal_net":"goal",
"hammer_and_pick":"hammer_pick",
"raised_hand_with_fingers_splayed":"hand_splayed",
- "reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse",
"raised_hand_with_fingers_splayed_tone1":"hand_splayed_tone1",
"raised_hand_with_fingers_splayed_tone2":"hand_splayed_tone2",
"raised_hand_with_fingers_splayed_tone3":"hand_splayed_tone3",
"raised_hand_with_fingers_splayed_tone4":"hand_splayed_tone4",
"raised_hand_with_fingers_splayed_tone5":"hand_splayed_tone5",
- "reversed_victory_hand":"hand_victory",
+ "shaking_hands":"handshake",
+ "shaking_hands_tone1":"handshake_tone1",
+ "shaking_hands_tone2":"handshake_tone2",
+ "shaking_hands_tone3":"handshake_tone3",
+ "shaking_hands_tone4":"handshake_tone4",
+ "shaking_hands_tone5":"handshake_tone5",
"face_with_head_bandage":"head_bandage",
"heavy_heart_exclamation_mark_ornament":"heart_exclamation",
- "heart_with_tip_on_the_left":"heart_tip",
"helmet_with_white_cross":"helmet_with_cross",
"house_buildings":"homes",
"hot_dog":"hotdog",
"derelict_house_building":"house_abandoned",
"hugging_face":"hugging",
- "circled_information_source":"info",
"desert_island":"island",
- "up_pointing_military_airplane":"jet_up",
+ "juggler":"juggling",
+ "juggler_tone1":"juggling_tone1",
+ "juggler_tone2":"juggling_tone2",
+ "juggler_tone3":"juggling_tone3",
+ "juggler_tone4":"juggling_tone4",
+ "juggler_tone5":"juggling_tone5",
"old_key":"key2",
- "wired_keyboard":"keyboard",
- "keyboard_and_mouse":"keyboard_mouse",
- "musical_keyboard_with_jacks":"keyboard_with_jacks",
"couplekiss_mm":"kiss_mm",
"couplekiss_ww":"kiss_ww",
+ "kiwifruit":"kiwi",
"satisfied":"laughing",
- "left_hand_telephone_receiver":"left_receiver",
+ "left_fist":"left_facing_fist",
+ "left_fist_tone1":"left_facing_fist_tone1",
+ "left_fist_tone2":"left_facing_fist_tone2",
+ "left_fist_tone3":"left_facing_fist_tone3",
+ "left_fist_tone4":"left_facing_fist_tone4",
+ "left_fist_tone5":"left_facing_fist_tone5",
"man_in_business_suit_levitating":"levitate",
"weight_lifter":"lifter",
"weight_lifter_tone1":"lifter_tone1",
@@ -371,9 +385,21 @@
"weight_lifter_tone3":"lifter_tone3",
"weight_lifter_tone4":"lifter_tone4",
"weight_lifter_tone5":"lifter_tone5",
- "light_mark":"light_check_mark",
"lion":"lion_face",
+ "liar":"lying_face",
+ "male_dancer":"man_dancing",
+ "male_dancer_tone1":"man_dancing_tone1",
+ "male_dancer_tone2":"man_dancing_tone2",
+ "male_dancer_tone3":"man_dancing_tone3",
+ "male_dancer_tone4":"man_dancing_tone4",
+ "male_dancer_tone5":"man_dancing_tone5",
+ "tuxedo_tone1":"man_in_tuxedo_tone1",
+ "tuxedo_tone2":"man_in_tuxedo_tone2",
+ "tuxedo_tone3":"man_in_tuxedo_tone3",
+ "tuxedo_tone4":"man_in_tuxedo_tone4",
+ "tuxedo_tone5":"man_in_tuxedo_tone5",
"world_map":"map",
+ "karate_uniform":"martial_arts_uniform",
"sports_medal":"medal",
"sign_of_the_horns":"metal",
"sign_of_the_horns_tone1":"metal_tone1",
@@ -388,21 +414,23 @@
"reversed_hand_with_middle_finger_extended_tone3":"middle_finger_tone3",
"reversed_hand_with_middle_finger_extended_tone4":"middle_finger_tone4",
"reversed_hand_with_middle_finger_extended_tone5":"middle_finger_tone5",
+ "glass_of_milk":"milk",
"money_mouth_face":"money_mouth",
- "lightning_mood_bubble":"mood_bubble_lightning",
- "lightning_mood":"mood_lightning",
+ "motorbike":"motor_scooter",
"racing_motorcycle":"motorcycle",
"snow_capped_mountain":"mountain_snow",
- "one_button_mouse":"mouse_one",
"three_button_mouse":"mouse_three_button",
+ "mother_christmas":"mrs_claus",
+ "mother_christmas_tone1":"mrs_claus_tone1",
+ "mother_christmas_tone2":"mrs_claus_tone2",
+ "mother_christmas_tone3":"mrs_claus_tone3",
+ "mother_christmas_tone4":"mrs_claus_tone4",
+ "mother_christmas_tone5":"mrs_claus_tone5",
+ "sick":"nauseated_face",
"nerd_face":"nerd",
- "three_networked_computers":"network",
"rolled_up_newspaper":"newspaper2",
- "note_page":"note",
- "empty_note_page":"note_empty",
- "note_pad":"notepad",
- "empty_note_pad":"notepad_empty",
"spiral_note_pad":"notepad_spiral",
+ "stop_sign":"octagonal_sign",
"oil_drum":"oil",
"grandma":"older_woman",
"grandma_tone1":"older_woman_tone1",
@@ -410,57 +438,66 @@
"grandma_tone3":"older_woman_tone3",
"grandma_tone4":"older_woman_tone4",
"grandma_tone5":"older_woman_tone5",
- "optical_disc_icon":"optical_disk",
"lower_left_paintbrush":"paintbrush",
"linked_paperclips":"paperclips",
"national_park":"park",
"double_vertical_bar":"pause_button",
"peace_symbol":"peace",
+ "shelled_peanut":"peanuts",
"lower_left_ballpoint_pen":"pen_ballpoint",
"lower_left_fountain_pen":"pen_fountain",
"memo":"pencil",
- "lower_left_pencil":"pencil3",
- "black_pennant":"pennant_black",
- "white_pennant":"pennant_white",
"table_tennis":"ping_pong",
- "no_piracy":"piracy",
"worship_symbol":"place_of_worship",
"shit":"poop",
"hankey":"poop",
"poo":"poop",
- "prohibited_sign":"prohibited",
+ "expecting_woman":"pregnant_woman",
+ "expecting_woman_tone1":"pregnant_woman_tone1",
+ "expecting_woman_tone2":"pregnant_woman_tone2",
+ "expecting_woman_tone3":"pregnant_woman_tone3",
+ "expecting_woman_tone4":"pregnant_woman_tone4",
+ "expecting_woman_tone5":"pregnant_woman_tone5",
"film_projector":"projector",
"racing_car":"race_car",
"radioactive_sign":"radioactive",
"railroad_track":"railway_track",
- "right_speaker_with_one_sound_wave":"right_speaker_one",
- "right_speaker_with_three_sound_waves":"right_speaker_three",
+ "back_of_hand":"raised_back_of_hand",
+ "back_of_hand_tone1":"raised_back_of_hand_tone1",
+ "back_of_hand_tone2":"raised_back_of_hand_tone2",
+ "back_of_hand_tone3":"raised_back_of_hand_tone3",
+ "back_of_hand_tone4":"raised_back_of_hand_tone4",
+ "back_of_hand_tone5":"raised_back_of_hand_tone5",
+ "rhinoceros":"rhino",
+ "right_fist":"right_facing_fist",
+ "right_fist_tone1":"right_facing_fist_tone1",
+ "right_fist_tone2":"right_facing_fist_tone2",
+ "right_fist_tone3":"right_facing_fist_tone3",
+ "right_fist_tone4":"right_facing_fist_tone4",
+ "right_fist_tone5":"right_facing_fist_tone5",
"robot_face":"robot",
+ "rolling_on_the_floor_laughing":"rofl",
"face_with_rolling_eyes":"rolling_eyes",
+ "green_salad":"salad",
+ "second_place_medal":"second_place",
+ "paella":"shallow_pan_of_food",
+ "shopping_trolley":"shopping_cart",
"skeleton":"skull",
"skull_and_crossbones":"skull_crossbones",
"slightly_frowning_face":"slight_frown",
"slightly_smiling_face":"slight_smile",
+ "sneeze":"sneezing_face",
"speaking_head_in_silhouette":"speaking_head",
- "left_speech_bubble":"speech_left",
- "right_speech_bubble":"speech_right",
- "three_speech_bubbles":"speech_three",
- "two_speech_bubbles":"speech_two",
"sleuth_or_spy":"spy",
"sleuth_or_spy_tone1":"spy_tone1",
"sleuth_or_spy_tone2":"spy_tone2",
"sleuth_or_spy_tone3":"spy_tone3",
"sleuth_or_spy_tone4":"spy_tone4",
"sleuth_or_spy_tone5":"spy_tone5",
- "portable_stereo":"stereo",
- "black_touchtone_telephone":"telephone_black",
- "white_touchtone_telephone":"telephone_white",
+ "stuffed_pita":"stuffed_flatbread",
"face_with_thermometer":"thermometer_face",
"thinking_face":"thinking",
- "left_thought_bubble":"thought_left",
- "right_thought_bubble":"thought_right",
- "reversed_thumbs_down_sign":"thumbs_down_reverse",
- "reversed_thumbs_up_sign":"thumbs_up_reverse",
+ "third_place_medal":"third_place",
"-1":"thumbsdown",
"-1_tone1":"thumbsdown_tone1",
"-1_tone2":"thumbsdown_tone2",
@@ -479,9 +516,7 @@
"hammer_and_wrench":"tools",
"next_track":"track_next",
"previous_track":"track_previous",
- "diesel_locomotive":"train_diesel",
- "triangle_with_rounded_corners":"triangle_round",
- "turned_ok_hand_sign":"turned_ok_hand",
+ "whisky":"tumbler_glass",
"unicorn_face":"unicorn",
"upside_down_face":"upside_down",
"funeral_urn":"urn",
@@ -494,6 +529,12 @@
"white_sun_behind_cloud":"white_sun_cloud",
"white_sun_behind_cloud_with_rain":"white_sun_rain_cloud",
"white_sun_with_small_cloud":"white_sun_small_cloud",
- "left_writing_hand":"writing_hand",
+ "wilted_flower":"wilted_rose",
+ "wrestling":"wrestlers",
+ "wrestling_tone1":"wrestlers_tone1",
+ "wrestling_tone2":"wrestlers_tone2",
+ "wrestling_tone3":"wrestlers_tone3",
+ "wrestling_tone4":"wrestlers_tone4",
+ "wrestling_tone5":"wrestlers_tone5",
"zipper_mouth_face":"zipper_mouth"
}
diff --git a/fixtures/emojis/digests.json b/fixtures/emojis/digests.json
index 50ee5089d8f..078d3413f33 100644
--- a/fixtures/emojis/digests.json
+++ b/fixtures/emojis/digests.json
@@ -60,16 +60,6 @@
"digest": "5544eace06b8e1b6ea91940e893e013d33d6b166e14e6d128a87f2cd2de88332"
},
{
- "name": "airplane_northeast",
- "unicode": "1F6EA",
- "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4"
- },
- {
- "name": "northeast_pointing_airplane",
- "unicode": "1F6EA",
- "digest": "fdddc2cd3618ec6661612581b8b93553cb086b0bb197e96aedf1bee8055e7bb4"
- },
- {
"name": "airplane_small",
"unicode": "1F6E9",
"digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
@@ -80,26 +70,6 @@
"digest": "1a2e07abbbe90d05cee7ff8dd52f443d595ccb38959f3089fe016b77e5d6de7d"
},
{
- "name": "airplane_small_up",
- "unicode": "1F6E8",
- "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a"
- },
- {
- "name": "up_pointing_small_airplane",
- "unicode": "1F6E8",
- "digest": "029752b29a757c087dec60f45ea242e974fc181129e20390d5d4a2f90442091a"
- },
- {
- "name": "airplane_up",
- "unicode": "1F6E7",
- "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77"
- },
- {
- "name": "up_pointing_airplane",
- "unicode": "1F6E7",
- "digest": "ec45d4dbfce1f75dc59339417b1dcf5f1e1359cd9d04ff233babf359a3330e77"
- },
- {
"name": "alarm_clock",
"unicode": "23F0",
"digest": "fef05a3cd1cddbeca4de8091b94bddb93790b03fa213da86c0eec420f8c49599"
@@ -165,16 +135,6 @@
"digest": "332493913891aa0eda2743b4bb16c4682400f249998bf34eb292246c9009e17f"
},
{
- "name": "anger_left",
- "unicode": "1F5EE",
- "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc"
- },
- {
- "name": "left_anger_bubble",
- "unicode": "1F5EE",
- "digest": "f2711991e8b386b2d5b12f296ce20a9b4b00ef91d6d67af2cf4e06abf2faa1dc"
- },
- {
"name": "anger_right",
"unicode": "1F5EF",
"digest": "8b049511ef3b1b28325841e2f87c60773eaf2f65cabba58d8b0ec3de9b10c0ae"
@@ -325,11 +285,6 @@
"digest": "c115e6613ebd718268aa31d265e017138b9fb58bbb8201eb3f40de2380e460aa"
},
{
- "name": "ascending_notes",
- "unicode": "1F39C",
- "digest": "33432042771d456338dda5d98e49322d3600f2cc9049963480c7c38d9de1ef0a"
- },
- {
"name": "asterisk",
"unicode": "002A-20E3",
"digest": "33d92093f2914448d5a939cf62e8ee3e32931923abdef5f0210e8a8150fa312d"
@@ -365,6 +320,11 @@
"digest": "6b6bb83b00707a314e46ff8eefbda40978a291ec7881caba1b1ee273f49c1368"
},
{
+ "name": "avocado",
+ "unicode": "1F951",
+ "digest": "bc1fb203d63b18985598400925de24050bb192afda1cbf0813f85cb139869eff"
+ },
+ {
"name": "b",
"unicode": "1F171",
"digest": "722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf"
@@ -420,6 +380,11 @@
"digest": "083e4e48b51092c28efb4532e840e1091b5d4b685c6e0f221aa0228f061cd91e"
},
{
+ "name": "bacon",
+ "unicode": "1F953",
+ "digest": "18ad3817f1f88a69706db5727a58e763dde6c21a2d4f184c3d728c32dc5fa05a"
+ },
+ {
"name": "badminton",
"unicode": "1F3F8",
"digest": "353eb7ee93decd9fe0072e4d78a5618d5e2d9e77a6e4de9fe171870d75e02a66"
@@ -445,41 +410,11 @@
"digest": "4175a56eca5c6458574a681e109b1403fbb143cf27f69ae6c1917650f3e08892"
},
{
- "name": "ballot_box_check",
- "unicode": "1F5F9",
- "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15"
- },
- {
- "name": "ballot_box_with_bold_check",
- "unicode": "1F5F9",
- "digest": "fc3ba16c009d963a4a0ea20a348ac98eee3c4c18c481df19a5ada0d1de7fcc15"
- },
- {
"name": "ballot_box_with_check",
"unicode": "2611",
"digest": "c98d6f3588dd87e2f318bbfe6c646399a905450edfd814edae4e5b1bddef2134"
},
{
- "name": "ballot_box_x",
- "unicode": "1F5F5",
- "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928"
- },
- {
- "name": "ballot_box_with_script_x",
- "unicode": "1F5F5",
- "digest": "861dcfc2361298262587b5d0e163fed96a55c44636361f5b4a9ab1d6502b8928"
- },
- {
- "name": "ballot_x",
- "unicode": "1F5F4",
- "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1"
- },
- {
- "name": "ballot_script_x",
- "unicode": "1F5F4",
- "digest": "0b73b89847eb82bcad5664644c8af237e0aef6c3d8c94b7a5df94e05d0ebf4e1"
- },
- {
"name": "bamboo",
"unicode": "1F38D",
"digest": "e4ee65088df43d7081b1ce6fd996f66f3e0accd88840855c47a98a22997823dd"
@@ -580,6 +515,11 @@
"digest": "c631cefc5d2a0a31bdb9f0a0d97ea68b1c6928e565468998403034644572a0b0"
},
{
+ "name": "bat",
+ "unicode": "1F987",
+ "digest": "8fc19e0d7d6f80906bdbc06d616a810de66180d96cf28070a53fa61b88904535"
+ },
+ {
"name": "bath",
"unicode": "1F6C0",
"digest": "33b371832f90aad50baf5296f3ad4cc081c319b279f989c74409903d8568e917"
@@ -760,6 +700,11 @@
"digest": "c2ba672994ad0f99d7fdc449f3fee45a2dca68a58f9fe95825b38465a30ef44e"
},
{
+ "name": "black_heart",
+ "unicode": "1F5A4",
+ "digest": "f334679168d6dd7328c28e9ae3cb2b1fca0e9c2777938d586bfe623db2a688b9"
+ },
+ {
"name": "black_joker",
"unicode": "1F0CF",
"digest": "d004b25f186494d5b2c65204caa9daecd749c840a0bea5718735e18109e5394d"
@@ -840,11 +785,6 @@
"digest": "9d912a9d1bb10dc7f2645b345ed09e90461e83df0de275acb806f1f75cef1fcf"
},
{
- "name": "book2",
- "unicode": "1F56E",
- "digest": "26d6b66a1957e7750b3e22eb2e46d0cc85932977bbb81d3d8482ec1ec58ee12b"
- },
- {
"name": "bookmark",
"unicode": "1F516",
"digest": "5705e3108259d6900649157843c50e22d0086c3630b291d3f942da1a736e3e3d"
@@ -875,16 +815,6 @@
"digest": "b93751a27b40f6185a22b3e8b413f0fe09b6010d1057c672e1a23088e0b8286f"
},
{
- "name": "bouquet2",
- "unicode": "1F395",
- "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d"
- },
- {
- "name": "bouquet_of_flowers",
- "unicode": "1F395",
- "digest": "1643ec51ff26fc1ac0c67859e202386398650bf2a996c82b68e1b73fa52abf7d"
- },
- {
"name": "bow",
"unicode": "1F647",
"digest": "33cd6da4d408f18d98bebc6a277dea8b914150e32ee472586ce3f1eb814462bd"
@@ -930,6 +860,16 @@
"digest": "737f2cdfa4ac964baade585a39771b18080bd5e9b55c8661d3518f468f344662"
},
{
+ "name": "boxing_glove",
+ "unicode": "1F94A",
+ "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
+ },
+ {
+ "name": "boxing_gloves",
+ "unicode": "1F94A",
+ "digest": "c914b2ce45f20afad66ad6f0d1b0750c4469e4f48b686dfc4aad1ec8d289c563"
+ },
+ {
"name": "boy",
"unicode": "1F466",
"digest": "7bc0173d8c88f3f12d41f213f7a3a9f5ebf65efad610fd5a2a31935128a6a6c1"
@@ -960,11 +900,6 @@
"digest": "0f76e97237203950da36c737dcc6f56dcd6c123401a8c817a0636376c7f38ef5"
},
{
- "name": "boys_symbol",
- "unicode": "1F6C9",
- "digest": "47fadbcb876ca436264ce2f3ebd1472bd68f55cc2b4833bf054335be9dc7a0f2"
- },
- {
"name": "bread",
"unicode": "1F35E",
"digest": "81739830f16f33e6a1dd7cc17c25df207846062bb5167bb8abed7fdd49268b86"
@@ -1035,21 +970,6 @@
"digest": "96e74842e919716b7bbbab57339bfd70f099a9bcb4710dffd7c80cf38a7bbff7"
},
{
- "name": "bullhorn",
- "unicode": "1F56B",
- "digest": "a4ca5cbfe299e8ccd148d17055d2d395cf8515e416bf771044c9a670509a8254"
- },
- {
- "name": "bullhorn_waves",
- "unicode": "1F56C",
- "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c"
- },
- {
- "name": "bullhorn_with_sound_waves",
- "unicode": "1F56C",
- "digest": "92493636cf086205d1e12cc19e613b84152ef10b8cd0215619a0fc813bfc9a7c"
- },
- {
"name": "burrito",
"unicode": "1F32F",
"digest": "b2cf81f1efdf87e674461f73f67cd4b58a5f695e65598d0dd3899f2597da43cf"
@@ -1075,6 +995,11 @@
"digest": "7fee96f1b68bb2c6002e47f2ed13c06baa6a3168441b9aca572db7ec45612f7b"
},
{
+ "name": "butterfly",
+ "unicode": "1F98B",
+ "digest": "a91b6598c17b44a8dc8935a1d99e25f4483ea41470cdd2da343039a9eec29ef1"
+ },
+ {
"name": "cactus",
"unicode": "1F335",
"digest": "2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd"
@@ -1085,16 +1010,6 @@
"digest": "b928902df8084210d51c1da36f9119164a325393c391b28cd8ea914e0b95c17b"
},
{
- "name": "calculator",
- "unicode": "1F5A9",
- "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c"
- },
- {
- "name": "pocket calculator",
- "unicode": "1F5A9",
- "digest": "01b47b5c69c12b65fa4f4c0d580f2a98280d6116f4ad2cf8be378759008bcc3c"
- },
- {
"name": "calendar",
"unicode": "1F4C6",
"digest": "9d990be27778daab041a3583edbd8f83fc8957e42a3aec729c0e2e224a8d05e3"
@@ -1110,6 +1025,66 @@
"digest": "441a0750eade7ce33e28e58bec76958990c412b68409fcdde59ebad1f25361bb"
},
{
+ "name": "call_me",
+ "unicode": "1F919",
+ "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
+ },
+ {
+ "name": "call_me_hand",
+ "unicode": "1F919",
+ "digest": "83d2ed96dcb8b4adf4f4d030ffd07e25ca16351e1a4fbefdf9f46f5ca496a55f"
+ },
+ {
+ "name": "call_me_tone1",
+ "unicode": "1F919-1F3FB",
+ "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
+ },
+ {
+ "name": "call_me_hand_tone1",
+ "unicode": "1F919-1F3FB",
+ "digest": "4a5748efa83e7294e8338b8795d4d315ff1cd31ead6759004d0eb330e50de8cd"
+ },
+ {
+ "name": "call_me_tone2",
+ "unicode": "1F919-1F3FC",
+ "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
+ },
+ {
+ "name": "call_me_hand_tone2",
+ "unicode": "1F919-1F3FC",
+ "digest": "54feaa6e3c5789ae6e15622127f0e0213234b4b886e1588ce95814348b1f1519"
+ },
+ {
+ "name": "call_me_tone3",
+ "unicode": "1F919-1F3FD",
+ "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
+ },
+ {
+ "name": "call_me_hand_tone3",
+ "unicode": "1F919-1F3FD",
+ "digest": "57e949b951e14843b712dab5a828f915ee255f5bb973db33946aab4057427419"
+ },
+ {
+ "name": "call_me_tone4",
+ "unicode": "1F919-1F3FE",
+ "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
+ },
+ {
+ "name": "call_me_hand_tone4",
+ "unicode": "1F919-1F3FE",
+ "digest": "f7787e933978a09c7b8ab8d3b1e1ab395aaae998c455e93bb3db24a4c8a60fe0"
+ },
+ {
+ "name": "call_me_tone5",
+ "unicode": "1F919-1F3FF",
+ "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
+ },
+ {
+ "name": "call_me_hand_tone5",
+ "unicode": "1F919-1F3FF",
+ "digest": "1fdb7d833d000b117d20d48142d3026a61cc9c8b712ebb498fa66bf75c74d7a5"
+ },
+ {
"name": "calling",
"unicode": "1F4F2",
"digest": "acf668c75c11c36686005788266524a972fa1c5bcf666ff3403d909edc5cee91"
@@ -1135,11 +1110,6 @@
"digest": "a42a4ff9521affa72db7b0f01da169b4cb6afb9db1c5dfad47dd4c507bfc30d9"
},
{
- "name": "cancellation_x",
- "unicode": "1F5D9",
- "digest": "cea2f7a48543207615ee06755ded62c2a95a7eaf7d7b68a3fc25e74d94e2c92c"
- },
- {
"name": "cancer",
"unicode": "264B",
"digest": "528c6f21df99a756b553d93a7f395b0f662b30a323affd05f0cedee8ff7b41d6"
@@ -1155,6 +1125,16 @@
"digest": "9cff4538918f60f770fceb96e964f5dc3ce31fd08ddd2ab3bfdf2981bfa74100"
},
{
+ "name": "canoe",
+ "unicode": "1F6F6",
+ "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
+ },
+ {
+ "name": "kayak",
+ "unicode": "1F6F6",
+ "digest": "56ca308cc2ad4827468cf58c4ccf6ef6b3382835a91e935540a2b973e01d2572"
+ },
+ {
"name": "capital_abcd",
"unicode": "1F520",
"digest": "a416d0b3f564037b680f801fb773b6eaf67225e2cbbfd2cb8a5db0de044321fa"
@@ -1185,14 +1165,69 @@
"digest": "c0e7059efc39a64233f774c02ddb1ab51888fff180f906ce13a6e4f9509672fe"
},
{
- "name": "cartridge",
- "unicode": "1F5AD",
- "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2"
+ "name": "carrot",
+ "unicode": "1F955",
+ "digest": "3a6fd98b63ee73d982a9cdacb08cf7b4014368cde8ffce6056b7df25a5a472b1"
+ },
+ {
+ "name": "cartwheel",
+ "unicode": "1F938",
+ "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
+ },
+ {
+ "name": "person_doing_cartwheel",
+ "unicode": "1F938",
+ "digest": "d78de3435e0b04a9b1a1048ae12e63e3248f9ace3a0db4d3bda584f22af18863"
+ },
+ {
+ "name": "cartwheel_tone1",
+ "unicode": "1F938-1F3FB",
+ "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
+ },
+ {
+ "name": "person_doing_cartwheel_tone1",
+ "unicode": "1F938-1F3FB",
+ "digest": "39a49781a269bb40d8efc8fd73c973b00fb2e192850ea6073062b5dea0cd5b74"
+ },
+ {
+ "name": "cartwheel_tone2",
+ "unicode": "1F938-1F3FC",
+ "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
+ },
+ {
+ "name": "person_doing_cartwheel_tone2",
+ "unicode": "1F938-1F3FC",
+ "digest": "6231eb35be45457fd648f8f4b79983f03705c9d983a18067f7e6d9ae47bc1958"
+ },
+ {
+ "name": "cartwheel_tone3",
+ "unicode": "1F938-1F3FD",
+ "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
+ },
+ {
+ "name": "person_doing_cartwheel_tone3",
+ "unicode": "1F938-1F3FD",
+ "digest": "ca483c78cc823811a8c279c501d9b283e4c990dafc5995ad40e68ecb0af554df"
+ },
+ {
+ "name": "cartwheel_tone4",
+ "unicode": "1F938-1F3FE",
+ "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
},
{
- "name": "tape_cartridge",
- "unicode": "1F5AD",
- "digest": "0b1625eea118060b51a70905c1eb3313ed632e989f70943eca16aa29fe8a34f2"
+ "name": "person_doing_cartwheel_tone4",
+ "unicode": "1F938-1F3FE",
+ "digest": "8253afb672431c84e498014c30babb00b9284bec773009e79f7f06aa7108643e"
+ },
+ {
+ "name": "cartwheel_tone5",
+ "unicode": "1F938-1F3FF",
+ "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
+ },
+ {
+ "name": "person_doing_cartwheel_tone5",
+ "unicode": "1F938-1F3FF",
+ "digest": "6fd92baff57c38b3adb6753d9e7e547e762971a8872fd3f1e71c6aaf0b1d3ab9"
},
{
"name": "cat",
@@ -1210,11 +1245,6 @@
"digest": "16363d8a34b873c12df6354b99f575cae3d80e0d27100ed7eea70f0310953c7b"
},
{
- "name": "celtic_cross",
- "unicode": "1F548",
- "digest": "187aac988d7e02085a15f31c4cc0ff25127be5b088e354e65c7b1152bffb40ff"
- },
- {
"name": "chains",
"unicode": "26D3",
"digest": "3884cdbc6f2b433062af06f942552e563231c24727a2f10fa280b3bb7aa614e2"
@@ -1230,6 +1260,16 @@
"digest": "9e6e8987f30a37ae0f3d7dab2f5eeb50aa32b4f31402b29315eb2994afc72457"
},
{
+ "name": "champagne_glass",
+ "unicode": "1F942",
+ "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
+ },
+ {
+ "name": "clinking_glass",
+ "unicode": "1F942",
+ "digest": "5a2e4773f7eb126a00122cbfa4dc535da51ce00e0bf0d8d6ff8bab8b3365f8d2"
+ },
+ {
"name": "chart",
"unicode": "1F4B9",
"digest": "a092dbc08f925b028286b2b495a5f59033b8537a586a694f46f4c1e7c3a1e27f"
@@ -1515,16 +1555,6 @@
"digest": "9fdef6a4939315c017b165e1dbac7710fb335df8c309be3fe2a011ef7fc28d74"
},
{
- "name": "clockwise_arrows",
- "unicode": "1F5D8",
- "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622"
- },
- {
- "name": "clockwise_right_and_left_semicircle_arrows",
- "unicode": "1F5D8",
- "digest": "67027b7e1a4d800a3ce7d731c21c098d1109d217159a27665eebb7e080fc2622"
- },
- {
"name": "closed_book",
"unicode": "1F4D5",
"digest": "b18288629d201bfdfc5d66ec47df89809d00642b15732757e6a04789f36a7d9f"
@@ -1585,6 +1615,16 @@
"digest": "7cbed2343c280ba3996082b3d0fb9d8cd57d6e62fe6c9ecb159f46b4a2e49151"
},
{
+ "name": "clown",
+ "unicode": "1F921",
+ "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
+ },
+ {
+ "name": "clown_face",
+ "unicode": "1F921",
+ "digest": "eea95687caabc9e808514c2450ba599e5e24ef47923dbec86f5297a64438e2e5"
+ },
+ {
"name": "clubs",
"unicode": "2663",
"digest": "b8cf72ecd8568ced077b475d94788fb282bdb06d25031b5d54dd63e25effb138"
@@ -1625,16 +1665,6 @@
"digest": "c970ce76b5607434895b0407bdaa93140f887930781a17dd7dcf16f711451d93"
},
{
- "name": "computer_old",
- "unicode": "1F5B3",
- "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3"
- },
- {
- "name": "old_personal_computer",
- "unicode": "1F5B3",
- "digest": "b27c30d74f205a8a3bd00a55ca17da7cf6ae3b65ae33e949755a4c6bd69a9fd3"
- },
- {
"name": "confetti_ball",
"unicode": "1F38A",
"digest": "a638b16f1acdbcf69edf760161b1bd7ff1fd5426c5b1203ad9d294dcc0701f10"
@@ -1665,6 +1695,11 @@
"digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
},
{
+ "name": "building_construction",
+ "unicode": "1F3D7",
+ "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
+ },
+ {
"name": "construction_worker",
"unicode": "1F477",
"digest": "8c094733987e7c4da8d3aa4588b530ae07042bd70cf337b1fd412a70ee8f0ed6"
@@ -1700,16 +1735,6 @@
"digest": "0d7f33ff7acc1cc3a81e6a786ff007df20da145e3070f338505dfed5100e9fcb"
},
{
- "name": "contruction_site",
- "unicode": "1F3D7",
- "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
- },
- {
- "name": "building_construction",
- "unicode": "1F3D7",
- "digest": "c611f0a5de10f000a0756935f226845c7292f19ff5581d1f7a7554316338bbcb"
- },
- {
"name": "convenience_store",
"unicode": "1F3EA",
"digest": "975dcf9b8e9e3fb1e29574b41300b9d96fd64703b3c18ff52f9f1875d1cf1b52"
@@ -1720,6 +1745,11 @@
"digest": "4bed3522bd50091ac5b68ca760661eb484d7f1b9c9d564d2097bd812b7f28ae4"
},
{
+ "name": "cooking",
+ "unicode": "1F373",
+ "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58"
+ },
+ {
"name": "cool",
"unicode": "1F192",
"digest": "5739a37341c782a4736adfce804e12776ae33081098a3d052d8ae9a64b4d22d1"
@@ -1820,6 +1850,16 @@
"digest": "e7a5131d7dee0f3356814b0ac1ea8ff280b12a7b580181e20ddb0b7eeb7e7339"
},
{
+ "name": "cowboy",
+ "unicode": "1F920",
+ "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
+ },
+ {
+ "name": "face_with_cowboy_hat",
+ "unicode": "1F920",
+ "digest": "1aabf23f6b95a9b772fdb8eb45b8ec93584a5357f9131c6eabc9d1b83fe67e89"
+ },
+ {
"name": "crab",
"unicode": "1F980",
"digest": "e6be16699fdb5d87f42f28f6cc141a44b7ffd834ecdd536813c4b5b86d3fc4a5"
@@ -1860,6 +1900,11 @@
"digest": "59cb4164c50b6bc9ae311ce6f7610467c1aaafa848b5fff7614f064715f91992"
},
{
+ "name": "croissant",
+ "unicode": "1F950",
+ "digest": "b751e287157a1e276617a841a5b5f7f1208ca226cfd8fa947f144390b65a5e16"
+ },
+ {
"name": "cross",
"unicode": "271D",
"digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
@@ -1870,36 +1915,6 @@
"digest": "a6b07c838fb75ef2ebefa2df6005e8d784753239ec03c37695a13e3b1954d653"
},
{
- "name": "cross_heavy",
- "unicode": "1F547",
- "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70"
- },
- {
- "name": "heavy_latin_cross",
- "unicode": "1F547",
- "digest": "2e37c26b9bad0beb019c7f3e7a3892352d0ad9ca1b90c4333d42e8d56680be70"
- },
- {
- "name": "cross_white",
- "unicode": "1F546",
- "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6"
- },
- {
- "name": "white_latin_cross",
- "unicode": "1F546",
- "digest": "3452e667010d7e49a51d7e1f4ba8ed4f303e33ed43255a051e9a18832a1efba6"
- },
- {
- "name": "crossbones",
- "unicode": "1F571",
- "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584"
- },
- {
- "name": "black_skull_and_crossbones",
- "unicode": "1F571",
- "digest": "f5e7ce293c1a3282711073e68f033a3876e8428d1218cb2f8294630f9124e584"
- },
- {
"name": "crossed_flags",
"unicode": "1F38C",
"digest": "2841c671075e6f1a79c61c2d716423159fb0bc0786e3fb0049697766533bf262"
@@ -1940,6 +1955,11 @@
"digest": "05f73b30b1e5b0fc66fb5dc6caddd2d547ee7b9d2f97513dc908ba1a2e352e30"
},
{
+ "name": "cucumber",
+ "unicode": "1F952",
+ "digest": "d1196e23f2f155ef5c1330f8497f40957a7357cb177127f457c5c471f0a23727"
+ },
+ {
"name": "cupid",
"unicode": "1F498",
"digest": "246e71f44c6ebc2e4f887e25438e4f894e8cc92e06069e711b893ff391abb658"
@@ -2050,16 +2070,16 @@
"digest": "3c70f1a77f2754f41c830e88d43b7d53c14311d64626ded164aa9ac7d2695790"
},
{
+ "name": "deer",
+ "unicode": "1F98C",
+ "digest": "7f4302ca68fd121ee73be48d0a0a0fb9e7e2741071a491ad2b7b0eab9f11ad25"
+ },
+ {
"name": "department_store",
"unicode": "1F3EC",
"digest": "4be910d2efe74d8ce2c1f41d7753c8873579faca83fcf779a4887d8ab9e5923b"
},
{
- "name": "descending_notes",
- "unicode": "1F39D",
- "digest": "f09c6a2e094b13bf91cc07b7b776e43348ccef9f91247ca36cc02e7d91098af0"
- },
- {
"name": "desert",
"unicode": "1F3DC",
"digest": "d4b1a11c5130debe042df6cc2b3389f15c68a5cb32dc1b3a82b78f733d0c9e4e"
@@ -2075,11 +2095,6 @@
"digest": "cde5bfb6c71bb7d663808a3561b24cb5b5560f95f510b40f81250cac1b21933e"
},
{
- "name": "desktop_window",
- "unicode": "1F5D4",
- "digest": "d5b6c4a847e2a96f97f50fd353a22cb121915cb1d7bbc0f02df38769819b6b7e"
- },
- {
"name": "diamond_shape_with_a_dot_inside",
"unicode": "1F4A0",
"digest": "e91323577ab89e95b0fa0b9272ea0c797b76908f24d36992630e9325273a4ce3"
@@ -2125,21 +2140,6 @@
"digest": "98b07fbbcdb438d1b8a755869fa2de8e180a77fce359ec830eb46d38ec3e67cb"
},
{
- "name": "document",
- "unicode": "1F5CE",
- "digest": "2cbca96cc69306a10f1a9b6505723e027239439d899f6b395dc43f3c37d2d777"
- },
- {
- "name": "document_text",
- "unicode": "1F5B9",
- "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72"
- },
- {
- "name": "document_with_text",
- "unicode": "1F5B9",
- "digest": "29407b12409c9673f3d89ef1f86ee50cbc7ed53b1870e33b4a29bbc609017f72"
- },
- {
"name": "dog",
"unicode": "1F436",
"digest": "3b31ce067b13e463284ce85536512cb1f8cd8b52fe73659f69971d0d6c1dfc11"
@@ -2205,11 +2205,36 @@
"digest": "e06ef69c29f0fb12481727c0b4124e700572d3d7955e173279320f43f286518d"
},
{
+ "name": "drooling_face",
+ "unicode": "1F924",
+ "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
+ },
+ {
+ "name": "drool",
+ "unicode": "1F924",
+ "digest": "5203cb05cd266d7a7c929ab40364ad68571d380d9c7ff93a8d6d55261abaa1ba"
+ },
+ {
"name": "droplet",
"unicode": "1F4A7",
"digest": "6475b4a4460a672c436a68f282ac97fb31e2934db4b80620063ee816159aa7c3"
},
{
+ "name": "drum",
+ "unicode": "1F941",
+ "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
+ },
+ {
+ "name": "drum_with_drumsticks",
+ "unicode": "1F941",
+ "digest": "0d0639980b1a5dcbf1c3e7ef47263fb6543b871242c58452a8c2f642525d9dd8"
+ },
+ {
+ "name": "duck",
+ "unicode": "1F986",
+ "digest": "8f8373798a7727368b32328e7a9a349727a949e7391ddd243b6456141a4f7e94"
+ },
+ {
"name": "dvd",
"unicode": "1F4C0",
"digest": "3b7903285d91277181c26fdc9df857761bbac509d352e320c2519ea3b132704f"
@@ -2225,6 +2250,11 @@
"digest": "39b5a57a2376e4a1137e381be02a1775bd580e0371438f5297a401ea634f1830"
},
{
+ "name": "eagle",
+ "unicode": "1F985",
+ "digest": "b44fd4f61b83c5114358a272343ac9b0eabbc70847f739bbdbf8aae3ade5bc1d"
+ },
+ {
"name": "ear",
"unicode": "1F442",
"digest": "4fdeb5a46e69311ecfd09c5b45c9018c24b625e28475cca8fa516b086ef952f8"
@@ -2276,8 +2306,8 @@
},
{
"name": "egg",
- "unicode": "1F373",
- "digest": "563ffd6cae381ce1e318cdacc54e70040d6a01a50d0db8aeb50edbbe413eac58"
+ "unicode": "1F95A",
+ "digest": "72b9c841af784e7cbccbbe48ba833df5cecdd284397c199cab079872e879d92f"
},
{
"name": "eggplant",
@@ -2300,6 +2330,16 @@
"digest": "bb0758e7cc0e357285937671a91489bd32ce9d248eecdcc9c275a53a66325b26"
},
{
+ "name": "eject",
+ "unicode": "23CF",
+ "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
+ },
+ {
+ "name": "eject_symbol",
+ "unicode": "23CF",
+ "digest": "eeb0cd23ead0c965e307de517a6805265f0c780c3e454e64bc4c1425dfe7548e"
+ },
+ {
"name": "electric_plug",
"unicode": "1F50C",
"digest": "b10ce87af86fa4f4022572ceb5ecd73bea867347a86832a7ea248364b0aad8d0"
@@ -2320,46 +2360,6 @@
"digest": "f5a512022a2f5280f372ff39c22cbda815f698710ca66f8f8c4d08418f98ca78"
},
{
- "name": "envelope_back",
- "unicode": "1F582",
- "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75"
- },
- {
- "name": "back_of_envelope",
- "unicode": "1F582",
- "digest": "bc60b6d375feee00758a94a05b42eeb165f4084b20eb3e6012b72faa221f7e75"
- },
- {
- "name": "envelope_flying",
- "unicode": "1F585",
- "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214"
- },
- {
- "name": "flying_envelope",
- "unicode": "1F585",
- "digest": "9d6b6ca4c08006062a6f11948de3e15b13cf5c458967e39a9358665a8e13e214"
- },
- {
- "name": "envelope_stamped",
- "unicode": "1F583",
- "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468"
- },
- {
- "name": "stamped_envelope",
- "unicode": "1F583",
- "digest": "f6102aea7283ddc136bfeb09589573420b9279105045fc6b965c1633c1297468"
- },
- {
- "name": "envelope_stamped_pen",
- "unicode": "1F586",
- "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33"
- },
- {
- "name": "pen_over_stamped_envelope",
- "unicode": "1F586",
- "digest": "80ea471318d1e04f8e525ff236b3cd4a4c864e66c6246b6aad77d92f56895f33"
- },
- {
"name": "envelope_with_arrow",
"unicode": "1F4E9",
"digest": "f8643212e6a94f58ccf2bcedc54c5fda8ebeab274f4a8803f253de5f50ddb1d6"
@@ -2415,6 +2415,36 @@
"digest": "1d5cae0b9b2e51e1de54295685d7f0c72ee794e2e6335a95b1d056c7e77260e8"
},
{
+ "name": "face_palm",
+ "unicode": "1F926",
+ "digest": "4ec873048b34b1bb34430724cf28e4bee6c0a9eee88ce39b9d1565047dc92420"
+ },
+ {
+ "name": "face_palm_tone1",
+ "unicode": "1F926-1F3FB",
+ "digest": "e93ef92b4c01dbea6c400e708e23dd36da92ccfbf5eb4f177b3b20c3a46bdc19"
+ },
+ {
+ "name": "face_palm_tone2",
+ "unicode": "1F926-1F3FC",
+ "digest": "22c8bf9fd9fa2ed9dca7a6397ed00ba6cfe9aeef2b0fb7b516ee4dda0df050ea"
+ },
+ {
+ "name": "face_palm_tone3",
+ "unicode": "1F926-1F3FD",
+ "digest": "c0b8bb9d2423e6787b6bdf1ca5a13f52853e4f48a9a1af0f2d4af1364fff022e"
+ },
+ {
+ "name": "face_palm_tone4",
+ "unicode": "1F926-1F3FE",
+ "digest": "f522ab186adcbb4549ea2c03500cdd7a86add548e43ebf7a54d58cc24deea072"
+ },
+ {
+ "name": "face_palm_tone5",
+ "unicode": "1F926-1F3FF",
+ "digest": "363507ae7178b5ec583635f47bcab10c897346f48b85d8759b1004c32cd8ad65"
+ },
+ {
"name": "factory",
"unicode": "1F3ED",
"digest": "c7aeb61ed8b0ac5c91d5197c73f1e2bb801921c22a76bb82c7659d990680dcb0"
@@ -2520,6 +2550,16 @@
"digest": "45aca538d3a9831a0c7de491e5656c17705c07b8f4ac8e85254656b608976016"
},
{
+ "name": "fencer",
+ "unicode": "1F93A",
+ "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
+ },
+ {
+ "name": "fencing",
+ "unicode": "1F93A",
+ "digest": "5db00fa456af9f6c7cb88d300579dd63e426bcb97ad25486b664aff25c688e21"
+ },
+ {
"name": "ferris_wheel",
"unicode": "1F3A1",
"digest": "24b4551b7b79a2a5fd73de61542f2b444f896a52030c5f29791c8fcfcc28b95c"
@@ -2550,54 +2590,64 @@
"digest": "4da212148cadb9c4ea91e60d2d8316e38cea99ef4f14afc023711dd7c54ade5a"
},
{
- "name": "finger_pointing_down",
- "unicode": "1F597",
- "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9"
+ "name": "fingers_crossed",
+ "unicode": "1F91E",
+ "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
},
{
- "name": "white_down_pointing_left_hand_index",
- "unicode": "1F597",
- "digest": "0c542ac3141e8f2e74767acd0eb399c2d68c779cb78bf16d437ad3b1f8134ad9"
+ "name": "hand_with_index_and_middle_finger_crossed",
+ "unicode": "1F91E",
+ "digest": "a5c797ead191b9712e185083266b455cdf09f6a34c10f8c51aa145e6073427e1"
},
{
- "name": "finger_pointing_down2",
- "unicode": "1F59F",
- "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee"
+ "name": "fingers_crossed_tone1",
+ "unicode": "1F91E-1F3FB",
+ "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
},
{
- "name": "sideways_white_down_pointing_index",
- "unicode": "1F59F",
- "digest": "c5b128a232cbf518544802a2ae1459368274297163721fa05d0103cf95b2b1ee"
+ "name": "hand_with_index_and_middle_fingers_crossed_tone1",
+ "unicode": "1F91E-1F3FB",
+ "digest": "db56d47bf887f2d8459a3aaba23f15c0087234ae5a54125052e7046e034a4988"
},
{
- "name": "finger_pointing_left",
- "unicode": "1F598",
- "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e"
+ "name": "fingers_crossed_tone2",
+ "unicode": "1F91E-1F3FC",
+ "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
},
{
- "name": "sideways_white_left_pointing_index",
- "unicode": "1F598",
- "digest": "d178ece691e2091be08db77fda9cf05462934628557358a8cb6222587b291f7e"
+ "name": "hand_with_index_and_middle_fingers_crossed_tone2",
+ "unicode": "1F91E-1F3FC",
+ "digest": "19f1bcca3991db7ed2037278c0baab6cd7f12aeaf2e0074de402c4d9e45c1899"
},
{
- "name": "finger_pointing_right",
- "unicode": "1F599",
- "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d"
+ "name": "fingers_crossed_tone3",
+ "unicode": "1F91E-1F3FD",
+ "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
},
{
- "name": "sideways_white_right_pointing_index",
- "unicode": "1F599",
- "digest": "a412a47544d8f401f9181f8826c5fa3d6b42a1d76f6926963c2d9cd2a01be06d"
+ "name": "hand_with_index_and_middle_fingers_crossed_tone3",
+ "unicode": "1F91E-1F3FD",
+ "digest": "895a3314f6a310f31f7e728bcca20ff834fbfac62ce00e27e3ea5ad0dfc1ba35"
},
{
- "name": "finger_pointing_up",
- "unicode": "1F59E",
- "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d"
+ "name": "fingers_crossed_tone4",
+ "unicode": "1F91E-1F3FE",
+ "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
},
{
- "name": "sideways_white_up_pointing_index",
- "unicode": "1F59E",
- "digest": "32c2ccab52aa318a47c816d1bcf9c076e667c9ef3e64ce37d7ba7e827238690d"
+ "name": "hand_with_index_and_middle_fingers_crossed_tone4",
+ "unicode": "1F91E-1F3FE",
+ "digest": "fcb5c4de2001d23a5df1b8702624d134b7f94e93e2dcc8adf6c1033c77722b0e"
+ },
+ {
+ "name": "fingers_crossed_tone5",
+ "unicode": "1F91E-1F3FF",
+ "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
+ },
+ {
+ "name": "hand_with_index_and_middle_fingers_crossed_tone5",
+ "unicode": "1F91E-1F3FF",
+ "digest": "50132c78d530b048c21be4e788b446872a79b3b3a91009db12f4021c44c8469d"
},
{
"name": "fire",
@@ -2615,19 +2665,19 @@
"digest": "c3a518f27d625e3b62dffa227eb82764bf0a147f10ec0e7f4f43f3f96751af20"
},
{
- "name": "fire_engine_oncoming",
- "unicode": "1F6F1",
- "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6"
+ "name": "fireworks",
+ "unicode": "1F386",
+ "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65"
},
{
- "name": "oncoming_fire_engine",
- "unicode": "1F6F1",
- "digest": "e2482c450136d373f74dfafddf502e0b675eb5d2e1e1c645f163db0e4d15fbb6"
+ "name": "first_place",
+ "unicode": "1F947",
+ "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
},
{
- "name": "fireworks",
- "unicode": "1F386",
- "digest": "b62ae08a00c0cc6eba8f9666c8fd9946ce57c3cfc01fe99542a8690a4a566a65"
+ "name": "first_place_medal",
+ "unicode": "1F947",
+ "digest": "e3de5d9f14f05544dbee5965cc2baa20e7b417a488c8a18598979038860fd901"
},
{
"name": "first_quarter_moon",
@@ -5300,41 +5350,11 @@
"digest": "ebf49007f367dc05580e9dab942e93e9dda12fa1dc2caa410ac7f8d8cd55d2a3"
},
{
- "name": "flip_phone",
- "unicode": "1F581",
- "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697"
- },
- {
- "name": "clamshell_mobile_phone",
- "unicode": "1F581",
- "digest": "be59efba4bc0759af5a726c06619090ef5071bf2541611d71691dedecee6c697"
- },
- {
- "name": "floppy_black",
- "unicode": "1F5AA",
- "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea"
- },
- {
- "name": "black_hard_shell_floppy_disk",
- "unicode": "1F5AA",
- "digest": "9022f51bb09c5130c6d46bb2accb159bed6f54d6fbffda6ecad62965ebc958ea"
- },
- {
"name": "floppy_disk",
"unicode": "1F4BE",
"digest": "4ee0b5bba41b9e301ed125d3ee1c263bef171ca499e6e1b89276b09af2bc03a0"
},
{
- "name": "floppy_white",
- "unicode": "1F5AB",
- "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0"
- },
- {
- "name": "white_hard_shell_floppy_disk",
- "unicode": "1F5AB",
- "digest": "ec79c400117c4506ef8cf3eebef6c42dd37e60b3079d3e98b6ccd06e517e2af0"
- },
- {
"name": "flower_playing_cards",
"unicode": "1F3B4",
"digest": "edba47c2e3051b2c7effd98794ec977174052782edcb491daec82a2b0d853869"
@@ -5355,21 +5375,6 @@
"digest": "bc3631a4e9e8473b92e842008937add2cd9ffad5b7d772ce759fb5ff6c0e3dca"
},
{
- "name": "folder",
- "unicode": "1F5C0",
- "digest": "8932141321911032ce8469ba85fe309b78384545c3b9946978b383670b956644"
- },
- {
- "name": "folder_open",
- "unicode": "1F5C1",
- "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685"
- },
- {
- "name": "open_folder",
- "unicode": "1F5C1",
- "digest": "74f3b484771c3d6ef61cf003de25c1a59b875afa46c057b5b1d92d9f99460685"
- },
- {
"name": "football",
"unicode": "1F3C8",
"digest": "ebd790471c3a28d3077818e3b31d915ffe443e06e299bc5cf0dd2534d080634c"
@@ -5410,6 +5415,16 @@
"digest": "ebee16e86bc9be843dfc72ab5372fb462f06be4486b5b25d7d4cac9b2c8b01c8"
},
{
+ "name": "fox",
+ "unicode": "1F98A",
+ "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
+ },
+ {
+ "name": "fox_face",
+ "unicode": "1F98A",
+ "digest": "e9903cb0396f7e49bdd2c384b38e614c13bfa576b3ecc1ec7b9819e4a40d91d1"
+ },
+ {
"name": "frame_photo",
"unicode": "1F5BC",
"digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
@@ -5420,29 +5435,19 @@
"digest": "d5074f748a15055ec1fb812c1e5e169e6e3cc73c522c54be1359b0e26c0fc75c"
},
{
- "name": "frame_tiles",
- "unicode": "1F5BD",
- "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0"
- },
- {
- "name": "frame_with_tiles",
- "unicode": "1F5BD",
- "digest": "34a5bb044b4b3ad94b116ad106f7b6747fb8612dc0e9f8ccd4313c2920508df0"
- },
- {
- "name": "frame_x",
- "unicode": "1F5BE",
- "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15"
+ "name": "free",
+ "unicode": "1F193",
+ "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa"
},
{
- "name": "frame_with_an_x",
- "unicode": "1F5BE",
- "digest": "2e427688fd70361c8c59787d0722ad68abe1c3f968258ee99c0c77ce4b8a8e15"
+ "name": "french_bread",
+ "unicode": "1F956",
+ "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
},
{
- "name": "free",
- "unicode": "1F193",
- "digest": "9973522457158362fc5bdd7da858e6371e28a8403d1ef9e4b6427195c7f72cfa"
+ "name": "baguette_bread",
+ "unicode": "1F956",
+ "digest": "47518a4312f57207b8e8c38188d4a2bd8b16830a885cfcf2d281cfab50c1bc6e"
},
{
"name": "fried_shrimp",
@@ -5560,16 +5565,21 @@
"digest": "f55e4b16a41b6f5e3c817a301420360ba4486e4e82e1092a56a3e3cc4069087d"
},
{
- "name": "girls_symbol",
- "unicode": "1F6CA",
- "digest": "2c55aee81defd7a1620ffeaad8d9bcc1835f19237c72c79633aec45671ddb9ff"
- },
- {
"name": "globe_with_meridians",
"unicode": "1F310",
"digest": "725bebeb3c09a9e3701ebe49e672dcfbf2b73575e05f0821263511577b013b75"
},
{
+ "name": "goal",
+ "unicode": "1F945",
+ "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
+ },
+ {
+ "name": "goal_net",
+ "unicode": "1F945",
+ "digest": "7088c432f276ff6f447dc0d431b9062b394fb401de1072fe59ca56267bfd6717"
+ },
+ {
"name": "goat",
"unicode": "1F410",
"digest": "d07e384d08529ddcaddd2710f2ad913e5665dc15d5f99c28e16dadd245a111e8"
@@ -5585,6 +5595,11 @@
"digest": "7d7ecc6e226596f646030a4109c2b0001ef0cc690e4863e450bf5d29e7a90344"
},
{
+ "name": "gorilla",
+ "unicode": "1F98D",
+ "digest": "4a564dc14f8ae5450d094f6410ec7f099a7f07dc5254b6395f44a35527bdb4b7"
+ },
+ {
"name": "grapes",
"unicode": "1F347",
"digest": "74d1a09ab411234a84d025a2e717e7ec5791bc02aad29853896d21c0f0283c50"
@@ -5735,16 +5750,6 @@
"digest": "c51a30cb7e575d29ffed16780a6c95ae3f300b8ac523012f4a6e116d68c1fd15"
},
{
- "name": "hand_splayed_reverse",
- "unicode": "1F591",
- "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db"
- },
- {
- "name": "reversed_raised_hand_with_fingers_splayed",
- "unicode": "1F591",
- "digest": "ff0af0fe9def7388adca6836e5958492282b1afae99f1b6e1e65d11ba68b96db"
- },
- {
"name": "hand_splayed_tone1",
"unicode": "1F590-1F3FB",
"digest": "c31fb44a982ed8808e1c311ec1b0b9c5afcb47f16bb1fc731dc483adf8f0d049"
@@ -5795,24 +5800,99 @@
"digest": "4b3a0aba7829772fec09f26d6facc19a2f822d2998015297b18b5cab85190ee2"
},
{
- "name": "hand_victory",
- "unicode": "1F594",
- "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735"
+ "name": "handbag",
+ "unicode": "1F45C",
+ "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45"
},
{
- "name": "reversed_victory_hand",
- "unicode": "1F594",
- "digest": "2d512ced4e8a438f2a346aed67310d3080f9828c748ade1be95943c32ba1c735"
+ "name": "handball",
+ "unicode": "1F93E",
+ "digest": "94ceb28024eb3259d8b137cafd7438773e717fbc04f5da810f85e43ca0fa9e00"
},
{
- "name": "handbag",
- "unicode": "1F45C",
- "digest": "45410a3eed0c2e3f68748d7649fa9e33a90f4e80d5291206bdd0b40380c6da45"
+ "name": "handball_tone1",
+ "unicode": "1F93E-1F3FB",
+ "digest": "8bec4de0d05c80e335e44d65598d186ca92696977353c9fd9c2a5efa122cb842"
+ },
+ {
+ "name": "handball_tone2",
+ "unicode": "1F93E-1F3FC",
+ "digest": "2ff4131e1e2f089b315d8e176c9348877c26c2bd03706fb75d41bc61bc99bf93"
+ },
+ {
+ "name": "handball_tone3",
+ "unicode": "1F93E-1F3FD",
+ "digest": "224a71f94dd37d3729325d11412334667a81422e21f6d7c008730ff350f51a80"
+ },
+ {
+ "name": "handball_tone4",
+ "unicode": "1F93E-1F3FE",
+ "digest": "a5f7a9db790565981bad2d0d9e09554c8c509a8179b4705a418300d58a7894b4"
+ },
+ {
+ "name": "handball_tone5",
+ "unicode": "1F93E-1F3FF",
+ "digest": "00404572d4683f2e8e8a494aa733e96fbec1723634d0a8cb8d75f2829a789d27"
+ },
+ {
+ "name": "handshake",
+ "unicode": "1F91D",
+ "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
+ },
+ {
+ "name": "shaking_hands",
+ "unicode": "1F91D",
+ "digest": "cb4b08b70560908f96bda0aecd2f4c966bea180f9b7200e4c81d342dc8d36087"
+ },
+ {
+ "name": "handshake_tone1",
+ "unicode": "1F91D-1F3FB",
+ "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
+ },
+ {
+ "name": "shaking_hands_tone1",
+ "unicode": "1F91D-1F3FB",
+ "digest": "40470e224683ba375ed8698c0cbd560556be5a8898237ddf504377a3a7e89ff0"
+ },
+ {
+ "name": "handshake_tone2",
+ "unicode": "1F91D-1F3FC",
+ "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
+ },
+ {
+ "name": "shaking_hands_tone2",
+ "unicode": "1F91D-1F3FC",
+ "digest": "77ed378243bf682f1f4f1a8caeabcbedf772f54631cc40ea46c099e46a499b18"
+ },
+ {
+ "name": "handshake_tone3",
+ "unicode": "1F91D-1F3FD",
+ "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
+ },
+ {
+ "name": "shaking_hands_tone3",
+ "unicode": "1F91D-1F3FD",
+ "digest": "81b95050f0878b617f5d2640e34031c26a0072e46ca5a688eb4356e48bc74c92"
+ },
+ {
+ "name": "handshake_tone4",
+ "unicode": "1F91D-1F3FE",
+ "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
+ },
+ {
+ "name": "shaking_hands_tone4",
+ "unicode": "1F91D-1F3FE",
+ "digest": "74919a6f026fbbd0ccdbdbd4288d1b2ef3bda8930e9142c07736db4a7f3ef345"
+ },
+ {
+ "name": "handshake_tone5",
+ "unicode": "1F91D-1F3FF",
+ "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
},
{
- "name": "hard_disk",
- "unicode": "1F5B4",
- "digest": "df8549d4281f5ae70fb6792a02c078e651764b0276aa43b7407236bd38fc21b4"
+ "name": "shaking_hands_tone5",
+ "unicode": "1F91D-1F3FF",
+ "digest": "a30d662bfad0074ca7e32cf6f7229b643b636c4beaec496777eb7e1d5b6fc470"
},
{
"name": "hash",
@@ -5880,16 +5960,6 @@
"digest": "8a1f28b97d661ca4cff5ee13889ca61b5fa745ccb590e80832b7d7701df101d6"
},
{
- "name": "heart_tip",
- "unicode": "1F394",
- "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204"
- },
- {
- "name": "heart_with_tip_on_the_left",
- "unicode": "1F394",
- "digest": "2178829e2c85accda55d2f685544587f6de5c8398a127ae1e08ff1c4ab282204"
- },
- {
"name": "heartbeat",
"unicode": "1F493",
"digest": "c9ec024943439d476df6f5ec3a6b30508365a7af3427671a80de3ef2f4f95ffe"
@@ -6145,16 +6215,6 @@
"digest": "310b7bdcca93452fe10c72c03d0aafa12b98e5d3408896d275d06d3693812c7a"
},
{
- "name": "info",
- "unicode": "1F6C8",
- "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca"
- },
- {
- "name": "circled_information_source",
- "unicode": "1F6C8",
- "digest": "59c35e77d5ee663c5d56f7d8af845ce8aeb9935e526ae4a06e02ae70e71212ca"
- },
- {
"name": "information_desk_person",
"unicode": "1F481",
"digest": "9f12a4a58a650e8e1d3836ef857003c3ccd42ad4203a2479eb95100bf6559064"
@@ -6250,16 +6310,6 @@
"digest": "f986ad32e419cca81c995f8371f0189d1490172a97ebbeac60054a1af08949c5"
},
{
- "name": "jet_up",
- "unicode": "1F6E6",
- "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948"
- },
- {
- "name": "up_pointing_military_airplane",
- "unicode": "1F6E6",
- "digest": "3708e5e034b1c64d1268d66527e13c369aa0f8903bce9172bef773b2d1940948"
- },
- {
"name": "joy",
"unicode": "1F602",
"digest": "75d7a05043523d290c46d3b313b19ed3c95271f1110bcf234cf13d4273625b08"
@@ -6275,6 +6325,66 @@
"digest": "671ee588f397a96f27056a67e6a06d6e8d22c2109ec57b2859badb5fec9cf8dd"
},
{
+ "name": "juggling",
+ "unicode": "1F939",
+ "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
+ },
+ {
+ "name": "juggler",
+ "unicode": "1F939",
+ "digest": "1f5dafa78de8b37f3df88fdf3084d2380666bd74ab2f449754d8724f6f8dbfa5"
+ },
+ {
+ "name": "juggling_tone1",
+ "unicode": "1F939-1F3FB",
+ "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
+ },
+ {
+ "name": "juggler_tone1",
+ "unicode": "1F939-1F3FB",
+ "digest": "b0b4d020148c896be69c28b08e3c486f6db270d138c7ccf4be362b29eb99878d"
+ },
+ {
+ "name": "juggling_tone2",
+ "unicode": "1F939-1F3FC",
+ "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
+ },
+ {
+ "name": "juggler_tone2",
+ "unicode": "1F939-1F3FC",
+ "digest": "cfe0c1649b2fdca03673e0e64f3a7d06d4bd49b8954c769aeb7eb88b70ec99f4"
+ },
+ {
+ "name": "juggling_tone3",
+ "unicode": "1F939-1F3FD",
+ "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
+ },
+ {
+ "name": "juggler_tone3",
+ "unicode": "1F939-1F3FD",
+ "digest": "7f87022722008bb265abe245e8157dc7a61944f5da62b3cf86f26ee1b3bdef63"
+ },
+ {
+ "name": "juggling_tone4",
+ "unicode": "1F939-1F3FE",
+ "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
+ },
+ {
+ "name": "juggler_tone4",
+ "unicode": "1F939-1F3FE",
+ "digest": "1f00da8c05582c95501cc6c3fe5ce0f9bfbc16789dcee59844a8fe7831198583"
+ },
+ {
+ "name": "juggling_tone5",
+ "unicode": "1F939-1F3FF",
+ "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
+ },
+ {
+ "name": "juggler_tone5",
+ "unicode": "1F939-1F3FF",
+ "digest": "a195bf734788eb7961c00dbc05255a49da8b9d5042fada29b26cc20393d3ce52"
+ },
+ {
"name": "kaaba",
"unicode": "1F54B",
"digest": "a4618782f9583f077bd383965f1c91b9985a949bb7b6cec7af22914e7f5e9ab6"
@@ -6296,38 +6406,8 @@
},
{
"name": "keyboard",
- "unicode": "1F5AE",
- "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16"
- },
- {
- "name": "wired_keyboard",
- "unicode": "1F5AE",
- "digest": "3b254cbf19946df3af05e501d11653d89fcda91684b7248d86186f842b83bf16"
- },
- {
- "name": "keyboard_mouse",
- "unicode": "1F5A6",
- "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3"
- },
- {
- "name": "keyboard_and_mouse",
- "unicode": "1F5A6",
- "digest": "95b523e55d8afeaeb06442bbe20e47f49643bb0c32d89a8cdbbccdead20532b3"
- },
- {
- "name": "keyboard_with_jacks",
- "unicode": "1F398",
- "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee"
- },
- {
- "name": "musical_keyboard_with_jacks",
- "unicode": "1F398",
- "digest": "e29a0d0b8018d13458469edca13c60a882a2817957c1aa11b050684c995a47ee"
- },
- {
- "name": "keycap_ten",
- "unicode": "1F51F",
- "digest": "c7c9491021740d2c17edddb856f79579b0b943d8dc85a2f48dbaac84f35b8a40"
+ "unicode": "2328",
+ "digest": "34da8ff62ca964142f9281b80123dbba74deaac8d77fa61758c30cfb36c31386"
},
{
"name": "kimono",
@@ -6385,6 +6465,16 @@
"digest": "f0f8636cb1a02b93cc72ce1b194b890fca823d91e35926b889be3ecfae79207f"
},
{
+ "name": "kiwi",
+ "unicode": "1F95D",
+ "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
+ },
+ {
+ "name": "kiwifruit",
+ "unicode": "1F95D",
+ "digest": "70a3a05f333d9455d2da12eed970bc3baae416286848fed8e5dd31b5be0819be"
+ },
+ {
"name": "knife",
"unicode": "1F52A",
"digest": "e6189e4843c6e80875b4952fcddb0c858f7c6039b9214bbec6a261a1358425df"
@@ -6450,19 +6540,69 @@
"digest": "e58cb714353e96a2891a5d97910ff79660e637af909b81c49c919d3735db55b4"
},
{
- "name": "left_luggage",
- "unicode": "1F6C5",
- "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf"
+ "name": "left_facing_fist",
+ "unicode": "1F91B",
+ "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
+ },
+ {
+ "name": "left_fist",
+ "unicode": "1F91B",
+ "digest": "7861be485beefae0de341df2f21576666e22f63511a033e785752f30c07291da"
+ },
+ {
+ "name": "left_facing_fist_tone1",
+ "unicode": "1F91B-1F3FB",
+ "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
+ },
+ {
+ "name": "left_fist_tone1",
+ "unicode": "1F91B-1F3FB",
+ "digest": "2e4c4dd96b0e4b46fe0f9ce5666344d266d0f17a8544cbae73d96638d1955296"
+ },
+ {
+ "name": "left_facing_fist_tone2",
+ "unicode": "1F91B-1F3FC",
+ "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
},
{
- "name": "left_receiver",
- "unicode": "1F57B",
- "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3"
+ "name": "left_fist_tone2",
+ "unicode": "1F91B-1F3FC",
+ "digest": "b96a63a801175ce98a75f0edad7b5574251a3fbbd894d8ab3f21aeeda366cc13"
},
{
- "name": "left_hand_telephone_receiver",
- "unicode": "1F57B",
- "digest": "8052e44951afee04c87296128744b5019ec783c9ed1a231f659af6c8ddaa50f3"
+ "name": "left_facing_fist_tone3",
+ "unicode": "1F91B-1F3FD",
+ "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
+ },
+ {
+ "name": "left_fist_tone3",
+ "unicode": "1F91B-1F3FD",
+ "digest": "99df84635513c2ebfef24df1bd3705233e02149eef788c7b82ca0548df6f6ea5"
+ },
+ {
+ "name": "left_facing_fist_tone4",
+ "unicode": "1F91B-1F3FE",
+ "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
+ },
+ {
+ "name": "left_fist_tone4",
+ "unicode": "1F91B-1F3FE",
+ "digest": "68954842ca725aec0aa39bce4aa81aad17ac30f5f298561dfa411feb07414cd3"
+ },
+ {
+ "name": "left_facing_fist_tone5",
+ "unicode": "1F91B-1F3FF",
+ "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
+ },
+ {
+ "name": "left_fist_tone5",
+ "unicode": "1F91B-1F3FF",
+ "digest": "a419b33fae82612dc860ff48950c0547a1642d4f0c94b6547324440837d3bb21"
+ },
+ {
+ "name": "left_luggage",
+ "unicode": "1F6C5",
+ "digest": "6625077767a51163ea20cbc299f3c13fd5ccf1b5ce365ee702ef1fef6be3dadf"
},
{
"name": "left_right_arrow",
@@ -6570,16 +6710,6 @@
"digest": "79b0edf6ce1fd024dd7f458e322ad8588af0b789a04cc1cf38380dc8b9c76f55"
},
{
- "name": "light_check_mark",
- "unicode": "1F5F8",
- "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b"
- },
- {
- "name": "light_mark",
- "unicode": "1F5F8",
- "digest": "7842b0df8c2b6703bed0cce5d2790d394eec7120b2a245a76f375528f2729a7b"
- },
- {
"name": "light_rail",
"unicode": "1F688",
"digest": "2f30b23a738371690b2f00d96ddb5ceb90a1442b5478754626a3dfa263ed2fc1"
@@ -6605,16 +6735,16 @@
"digest": "8740d8086525c7a836d64625a6915cc1c59af69ba143456dbb59e0179276895e"
},
{
- "name": "lips2",
- "unicode": "1F5E2",
- "digest": "c6ba915982ac47d8aaf14ad3605949df95588acfb4e147bf608f8c1714cdf19b"
- },
- {
"name": "lipstick",
"unicode": "1F484",
"digest": "751dcb22706a796033b13a2ccb94304236ec13207ad4d011e02d230ae33ab5c1"
},
{
+ "name": "lizard",
+ "unicode": "1F98E",
+ "digest": "fb9191f9eab58b8403d4c4626ccbb14ba05c1f6944011751a8edcc4dd03c66e6"
+ },
+ {
"name": "lock",
"unicode": "1F512",
"digest": "043b4fc0b8c79d47a07d91308e628e1ac262aea6c1ec05e6b84bf7bcdf89dc83"
@@ -6660,6 +6790,16 @@
"digest": "a065d00a416e297c168b0a675cafcf492fedf94865cb21801a1be5a3914593d4"
},
{
+ "name": "lying_face",
+ "unicode": "1F925",
+ "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
+ },
+ {
+ "name": "liar",
+ "unicode": "1F925",
+ "digest": "ce836170165e1b70938273f289c02c2106873cd9ab5472dbcd487c2f9f53f13d"
+ },
+ {
"name": "m",
"unicode": "24C2",
"digest": "54588ac2b7fcd53a96f17124e9de69b617613fcd5af9ad2930a094cb795bb9f4"
@@ -6705,6 +6845,121 @@
"digest": "42b882d2c6aa095f1afcf901203838d95c1908bdc725519779186b9c33c728d7"
},
{
+ "name": "man_dancing",
+ "unicode": "1F57A",
+ "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
+ },
+ {
+ "name": "male_dancer",
+ "unicode": "1F57A",
+ "digest": "9f632ee0c886d5f03c61e5f3a27668262c0cc2693b857a91c23c1e5ea3785b9e"
+ },
+ {
+ "name": "man_dancing_tone1",
+ "unicode": "1F57A-1F3FB",
+ "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
+ },
+ {
+ "name": "male_dancer_tone1",
+ "unicode": "1F57A-1F3FB",
+ "digest": "6c56a16cb105bcdd97472645b3a351cebdbb1132cbfd18b0118f289db5fbe741"
+ },
+ {
+ "name": "man_dancing_tone2",
+ "unicode": "1F57A-1F3FC",
+ "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
+ },
+ {
+ "name": "male_dancer_tone2",
+ "unicode": "1F57A-1F3FC",
+ "digest": "ed7e78c14d205a03fdd5581e5213add69a55e13b4cbaf76a6d5a0d6c80f53327"
+ },
+ {
+ "name": "man_dancing_tone3",
+ "unicode": "1F57A-1F3FD",
+ "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
+ },
+ {
+ "name": "male_dancer_tone3",
+ "unicode": "1F57A-1F3FD",
+ "digest": "13b45403e11800163406206eedeb8b579cc83eca2f60246be97e099164387bc8"
+ },
+ {
+ "name": "man_dancing_tone4",
+ "unicode": "1F57A-1F3FE",
+ "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
+ },
+ {
+ "name": "male_dancer_tone4",
+ "unicode": "1F57A-1F3FE",
+ "digest": "f6feb1b0b83565fadcdd1a8737d3daa08893e919547d2a06de899160162d9c4a"
+ },
+ {
+ "name": "man_dancing_tone5",
+ "unicode": "1F57A-1F3FF",
+ "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
+ },
+ {
+ "name": "male_dancer_tone5",
+ "unicode": "1F57A-1F3FF",
+ "digest": "fe20a9ed9ba991653b4d0683de347ed7c226a5d75610307584a2ddd6fcd1e3f2"
+ },
+ {
+ "name": "man_in_tuxedo",
+ "unicode": "1F935",
+ "digest": "4d451a971dfefedc4830ba78e19b123f250e09ae65baddccdc56c0f8aa3a9b50"
+ },
+ {
+ "name": "man_in_tuxedo_tone1",
+ "unicode": "1F935-1F3FB",
+ "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
+ },
+ {
+ "name": "tuxedo_tone1",
+ "unicode": "1F935-1F3FB",
+ "digest": "2814833334fb211ae2ecb1fb5964e9752282d0fb4d7f3477de5dd2a4f812a793"
+ },
+ {
+ "name": "man_in_tuxedo_tone2",
+ "unicode": "1F935-1F3FC",
+ "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
+ },
+ {
+ "name": "tuxedo_tone2",
+ "unicode": "1F935-1F3FC",
+ "digest": "cd1bab9ee0e2335d3cd99d51216cccdc4fc3c2cf20129b8b7e11a51a77258f68"
+ },
+ {
+ "name": "man_in_tuxedo_tone3",
+ "unicode": "1F935-1F3FD",
+ "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
+ },
+ {
+ "name": "tuxedo_tone3",
+ "unicode": "1F935-1F3FD",
+ "digest": "f387775f925fe60b9f3e7cad63a55d4d196ddd41658029a70440d14c17cb99f9"
+ },
+ {
+ "name": "man_in_tuxedo_tone4",
+ "unicode": "1F935-1F3FE",
+ "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
+ },
+ {
+ "name": "tuxedo_tone4",
+ "unicode": "1F935-1F3FE",
+ "digest": "08debd7a573d1201aee8a2f281ef7cb638d4a2a096222150391f36963f07c622"
+ },
+ {
+ "name": "man_in_tuxedo_tone5",
+ "unicode": "1F935-1F3FF",
+ "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
+ },
+ {
+ "name": "tuxedo_tone5",
+ "unicode": "1F935-1F3FF",
+ "digest": "e3b10e0619f0911cf9b665a265f4ef829b8f6ba6e9c3a021d0539a27e315f8fe"
+ },
+ {
"name": "man_tone1",
"unicode": "1F468-1F3FB",
"digest": "7053e265fa7d2594de54a6c5d06c21795b9a7dfb36a1c5594ca43c4c6cc56504"
@@ -6810,6 +7065,16 @@
"digest": "72629a205e33f89337815ad7e51bb5c73947d1a9f98afe5072bdf4846827ae72"
},
{
+ "name": "martial_arts_uniform",
+ "unicode": "1F94B",
+ "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
+ },
+ {
+ "name": "karate_uniform",
+ "unicode": "1F94B",
+ "digest": "a1ae797b31081425b388ab31efc635d8eb73a40980fd0fae4708aa5313e2a964"
+ },
+ {
"name": "mask",
"unicode": "1F637",
"digest": "1b58af9ae599308aabf41bbd38f599fa896bd9fe5df7a40be9f2dc7e0e230600"
@@ -7030,6 +7295,16 @@
"digest": "5da18351dc14b66cfc070148c83b7c8e67e6b1e3f515ae501133c38ee5c28d3d"
},
{
+ "name": "milk",
+ "unicode": "1F95B",
+ "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
+ },
+ {
+ "name": "glass_of_milk",
+ "unicode": "1F95B",
+ "digest": "38b28ea40399601fabc95bac5eaaf5a9e4e25548ec80325bd5069395ea884f85"
+ },
+ {
"name": "milky_way",
"unicode": "1F30C",
"digest": "17405ff31d94b13a1fb0adcda204b8adb95ca340bc3980d9ad9f42ba1e366e7d"
@@ -7085,31 +7360,6 @@
"digest": "2c9f185babcb4001fcef2b8dfc4a32126729843084d0076c3e3ccdc845ab23ad"
},
{
- "name": "mood_bubble",
- "unicode": "1F5F0",
- "digest": "1df7061217e478d43ab9a87d4f351c4ca56705acd6b4e0b0bedfdece77635f1b"
- },
- {
- "name": "mood_bubble_lightning",
- "unicode": "1F5F1",
- "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9"
- },
- {
- "name": "lightning_mood_bubble",
- "unicode": "1F5F1",
- "digest": "4af3e4e53eaa328b0d20542ab31705a74bf9fd368cd0673b706838ce1681d3c9"
- },
- {
- "name": "mood_lightning",
- "unicode": "1F5F2",
- "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f"
- },
- {
- "name": "lightning_mood",
- "unicode": "1F5F2",
- "digest": "6784635e81ec722fd50a1c2a23b0f9679e4bf1b5ae2b5a01eeb995bc1f7a426f"
- },
- {
"name": "mortar_board",
"unicode": "1F393",
"digest": "d7fbe41d4b340d3564e484aec46a22c9613521414b2ba6eece2180db4d23e410"
@@ -7120,6 +7370,16 @@
"digest": "5f3d3de7feac953a70a318113531c2857d760a516c3d8d6f42d2a3b3b67ed196"
},
{
+ "name": "motor_scooter",
+ "unicode": "1F6F5",
+ "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
+ },
+ {
+ "name": "motorbike",
+ "unicode": "1F6F5",
+ "digest": "e2dc7c981744a71f46858bd0858ff91af704ac06425ed80377bc3b119e57c872"
+ },
+ {
"name": "motorboat",
"unicode": "1F6E5",
"digest": "81c156643528c5a94a12d6d478e52a019f5a4e3eb58ee365cdd9d2361a7fdb01"
@@ -7210,16 +7470,6 @@
"digest": "f3ed37b639b7c16aae49502bd423f9fdeabaf15bc6f0f74063954b189e176b5d"
},
{
- "name": "mouse_one",
- "unicode": "1F5AF",
- "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0"
- },
- {
- "name": "one_button_mouse",
- "unicode": "1F5AF",
- "digest": "e0d2055ccba489d24e0c0b6d2f22793efe48a734b0fd50f5af88f721b40665c0"
- },
- {
"name": "mouse_three_button",
"unicode": "1F5B1",
"digest": "3724341ac5ad0d01027ef1575db64f1db7619f590ca6ada960d1f2c18dc7fc6a"
@@ -7240,6 +7490,66 @@
"digest": "2c1d0662c95928936e6b9ab5a40c6110ff1cea5339f2803c7b63aabc76115afb"
},
{
+ "name": "mrs_claus",
+ "unicode": "1F936",
+ "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
+ },
+ {
+ "name": "mother_christmas",
+ "unicode": "1F936",
+ "digest": "1f72f586ca75bd7ebb4150cdcc8199a930c32fa4b81510cb8d200f1b3ddd4076"
+ },
+ {
+ "name": "mrs_claus_tone1",
+ "unicode": "1F936-1F3FB",
+ "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
+ },
+ {
+ "name": "mother_christmas_tone1",
+ "unicode": "1F936-1F3FB",
+ "digest": "244596919e0fed050203cf9e040899de323d7821235929f175852439927bd129"
+ },
+ {
+ "name": "mrs_claus_tone2",
+ "unicode": "1F936-1F3FC",
+ "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
+ },
+ {
+ "name": "mother_christmas_tone2",
+ "unicode": "1F936-1F3FC",
+ "digest": "8cde96e8521f3a90262a7f5f8a2989a9590d9a02cda2c37e92335dc05975c18d"
+ },
+ {
+ "name": "mrs_claus_tone3",
+ "unicode": "1F936-1F3FD",
+ "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
+ },
+ {
+ "name": "mother_christmas_tone3",
+ "unicode": "1F936-1F3FD",
+ "digest": "c39cd4346d4581799dd0e9a6447c91a954a75747bf2682c8e4d79c3b0fcf7405"
+ },
+ {
+ "name": "mrs_claus_tone4",
+ "unicode": "1F936-1F3FE",
+ "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
+ },
+ {
+ "name": "mother_christmas_tone4",
+ "unicode": "1F936-1F3FE",
+ "digest": "84c85cf54559ea2d78d196fee96149a249af4f959b78e223a0ec4fb72abdbcab"
+ },
+ {
+ "name": "mrs_claus_tone5",
+ "unicode": "1F936-1F3FF",
+ "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
+ },
+ {
+ "name": "mother_christmas_tone5",
+ "unicode": "1F936-1F3FF",
+ "digest": "ce26c0e0645713b17e7497d9f2d0484cc5477564dae99320cabf04d160d3b2ff"
+ },
+ {
"name": "muscle",
"unicode": "1F4AA",
"digest": "e4ce52757b2b7982e2516e0e8bf2e2253617cc9f3e6178f1887c61c9039461ba"
@@ -7330,6 +7640,16 @@
"digest": "f9f6a4895ff0be8fb2ccc7ad195b94e9650f742f66ead999e90724cfb77af628"
},
{
+ "name": "nauseated_face",
+ "unicode": "1F922",
+ "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
+ },
+ {
+ "name": "sick",
+ "unicode": "1F922",
+ "digest": "f8471cf4720948d8246ec9d30e29783e819f90e3cfe8b1ba628671a1aad1a91c"
+ },
+ {
"name": "necktie",
"unicode": "1F454",
"digest": "01bb18dc8bfe787daa9613b5d09988cd5a065449ef906099ce3cb308c8a7da68"
@@ -7350,16 +7670,6 @@
"digest": "9e5f3c93db25cf1d0f9d6e6bd2993161afec6c30573ba3fe85e13b8c84483d66"
},
{
- "name": "network",
- "unicode": "1F5A7",
- "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06"
- },
- {
- "name": "three_networked_computers",
- "unicode": "1F5A7",
- "digest": "1dbaa54deeb2328fd8a3f044e450c97ac3ff39627c598bb2f4312d677482ee06"
- },
- {
"name": "neutral_face",
"unicode": "1F610",
"digest": "7449430a60619956573e9dc80834045296f2b99853737b6c7794c785ff53d64e"
@@ -7515,26 +7825,6 @@
"digest": "1e0f9842e0f8ad5805eabd3f35a6038b7a2e49d566a1f5c17271f9cdf467ca60"
},
{
- "name": "note",
- "unicode": "1F5C9",
- "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af"
- },
- {
- "name": "note_page",
- "unicode": "1F5C9",
- "digest": "073660fdaa02ecf98d04f61f8d65d6cc447ccae3825fccaff19a2c99ebba52af"
- },
- {
- "name": "note_empty",
- "unicode": "1F5C6",
- "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be"
- },
- {
- "name": "empty_note_page",
- "unicode": "1F5C6",
- "digest": "06b56eeaca6349bbcf1020bea98f937450a7e086db65cd5d7497748e0fb607be"
- },
- {
"name": "notebook",
"unicode": "1F4D3",
"digest": "fc679d3728f86073d1607a926885dd8b0261132f5c4a0322f1e46ea9f95c8cb8"
@@ -7545,26 +7835,6 @@
"digest": "d822eda4b49cbfa399b36f134c1a0b8dcfdd27ed89f12c50bc18f6f0a9aa56ef"
},
{
- "name": "notepad",
- "unicode": "1F5CA",
- "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e"
- },
- {
- "name": "note_pad",
- "unicode": "1F5CA",
- "digest": "85069e2d13540886457368a57295072aec44c7137d9223bfcf908ce1f0e5124e"
- },
- {
- "name": "notepad_empty",
- "unicode": "1F5C7",
- "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af"
- },
- {
- "name": "empty_note_pad",
- "unicode": "1F5C7",
- "digest": "8be5053e74c13d8220917c5aee1f4afdecb001612886438f283b0c2a0fecf6af"
- },
- {
"name": "notepad_spiral",
"unicode": "1F5D2",
"digest": "c6a8e16aa62474cef13e5659fddb4afc57e3f79635e32e6020edbee2b5b50f18"
@@ -7600,6 +7870,16 @@
"digest": "1a9ca9848d4fb75852addfc10bf84eccf7caa5339714b90e3de4cb6f2518465e"
},
{
+ "name": "octagonal_sign",
+ "unicode": "1F6D1",
+ "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
+ },
+ {
+ "name": "stop_sign",
+ "unicode": "1F6D1",
+ "digest": "9f6927048e1f9da57f89d1ae1eb86fa4ab7abdbabca756a738a799e948d0b3f9"
+ },
+ {
"name": "octopus",
"unicode": "1F419",
"digest": "0fcc65c12f4b29ea75a8c4823d20838a7e6db6978fdcb536943072aa1460bc59"
@@ -7860,16 +8140,6 @@
"digest": "6112e2a1656b1cb8bd9a8b0dfa6cbf66d30cae671710a9ef75c821de344aab2b"
},
{
- "name": "optical_disk",
- "unicode": "1F5B8",
- "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d"
- },
- {
- "name": "optical_disc_icon",
- "unicode": "1F5B8",
- "digest": "df8c10028d29d65f144a6b789d1c3294e7b3293554c4c30d28d72dc7ba8d9a5d"
- },
- {
"name": "orange_book",
"unicode": "1F4D9",
"digest": "41141b08d2beceded21a94795431603c47fd7d42a3a472a2aa8b2bb25fa87ebf"
@@ -7885,6 +8155,11 @@
"digest": "e47cb481a0ffcb39996f32fd313e19b362a91d8dda15ffca48ac23a3b5bb5baf"
},
{
+ "name": "owl",
+ "unicode": "1F989",
+ "digest": "f62ec1ad23ad9038966eea8d8b79660ac212f291af2e89bcdb0fdc683caf41e5"
+ },
+ {
"name": "ox",
"unicode": "1F402",
"digest": "d13bc60552190bb9936bf32d681bdc742439b702a09cfc62137ea09a98624aed"
@@ -7895,11 +8170,6 @@
"digest": "e82bf5accebb65136e897c15607eef635fb79fd7b2d8c8e19a9eb00b6786918c"
},
{
- "name": "page",
- "unicode": "1F5CF",
- "digest": "cc745056525f59d9128d1d03b14770376bb09ab64b8ef4ac994ab7f38efd4783"
- },
- {
"name": "page_facing_up",
"unicode": "1F4C4",
"digest": "3884868bdcb2f29615b09a13a30385cbc5269379094a54b5a7e8a5f4e8ce905a"
@@ -7915,11 +8185,6 @@
"digest": "e21c756cc1c58ebc1b37ebcd38e22a25b31e2e81306c6f18285d6a7671f9eb12"
},
{
- "name": "pages",
- "unicode": "1F5D0",
- "digest": "05bd47b78f089389356d9d839c736843f56b959ab4277056606ffcbb013390bc"
- },
- {
"name": "paintbrush",
"unicode": "1F58C",
"digest": "fc0da7a25b726b8be9dd6467953e27293d2313a21eeff21424c2a19be614fff2"
@@ -7935,6 +8200,11 @@
"digest": "90fedafd62fe0abf51325174d0f293ebb9a4794913b9ba93b12f2d0119056df1"
},
{
+ "name": "pancakes",
+ "unicode": "1F95E",
+ "digest": "5256b4832431e8a88555796b1a9726f12d909a26fb2bdc3a0abff76412c45903"
+ },
+ {
"name": "panda_face",
"unicode": "1F43C",
"digest": "56a4b84abe983bd6569be1b81ac5e43071015fd308389a16b92231310ae56a5b"
@@ -8010,6 +8280,16 @@
"digest": "768d1f4f29e1e06aff5abb29043be83087ded16427ce6a2d0f682814e665e311"
},
{
+ "name": "peanuts",
+ "unicode": "1F95C",
+ "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
+ },
+ {
+ "name": "shelled_peanut",
+ "unicode": "1F95C",
+ "digest": "e2384846b6e4a6c3a56e991ebb749cb68b330ac00a9e9d888b2c39105ff7ff5d"
+ },
+ {
"name": "pear",
"unicode": "1F350",
"digest": "b7c9cf90bb979649b863d2f4132f1b51f6f8107d42e08fb8b4033fea32844948"
@@ -8050,41 +8330,11 @@
"digest": "9ca1b56b5726f472b1f1b23050ed163e213916dac379d22e38e4c8358fe871e0"
},
{
- "name": "pencil3",
- "unicode": "1F589",
- "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c"
- },
- {
- "name": "lower_left_pencil",
- "unicode": "1F589",
- "digest": "52c1ba1228917eb491ac1745a495e0fdafba6b985a81caba250f71d1f94c725c"
- },
- {
"name": "penguin",
"unicode": "1F427",
"digest": "a1800ab931d6dc84a9c89bfab2c815198025c276d952509c55b18dd20bd9d316"
},
{
- "name": "pennant_black",
- "unicode": "1F3F2",
- "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269"
- },
- {
- "name": "black_pennant",
- "unicode": "1F3F2",
- "digest": "cd3c33bfc3c7fbe84b98d2d481d56a7bf5488ff94afadd8b5a0e454768b80269"
- },
- {
- "name": "pennant_white",
- "unicode": "1F3F1",
- "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0"
- },
- {
- "name": "white_pennant",
- "unicode": "1F3F1",
- "digest": "818b1be73540f2cfeb1c514e1ee75d18715af317f0db817d9ae081b9ea33d4b0"
- },
- {
"name": "pensive",
"unicode": "1F614",
"digest": "d237deff9f5ead8a0b281b7e5c6f4b82e98cc30c80c86c22c3fdc6160090b2f2"
@@ -8230,16 +8480,6 @@
"digest": "943a858bd054c81a08a08951f8351c27c8009b85a9359729c7362868298b58e1"
},
{
- "name": "piracy",
- "unicode": "1F572",
- "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9"
- },
- {
- "name": "no_piracy",
- "unicode": "1F572",
- "digest": "f42955ba75c598392e5e258be49968d858c876e0d6e7aa9dc795f7e8cff42be9"
- },
- {
"name": "pisces",
"unicode": "2653",
"digest": "453c3915122a4b6b32867056d2447be48675a84469145c88d52f8007fcb0861a"
@@ -8470,6 +8710,11 @@
"digest": "dbe80d9637837377cc2a290da2e895f81a3108cc18b049e3d87212402c1c2098"
},
{
+ "name": "potato",
+ "unicode": "1F954",
+ "digest": "a56a69f36f3a0793f278726d92c0cea2960554f3062ef1a0904526a04511d8e1"
+ },
+ {
"name": "pouch",
"unicode": "1F45D",
"digest": "9f012b90310b4a072b6a8fa2c64def087b5f7ffffaafc36e1856ba943a170351"
@@ -8525,6 +8770,96 @@
"digest": "80177091264430cbcf7c994fbe5ee17319d1a58d933636cc752a54dafcf98a05"
},
{
+ "name": "pregnant_woman",
+ "unicode": "1F930",
+ "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
+ },
+ {
+ "name": "expecting_woman",
+ "unicode": "1F930",
+ "digest": "49abb86409103338bdb6ae43c13a78ca2dc9cd158a26df35eadd0da3c84a4352"
+ },
+ {
+ "name": "pregnant_woman_tone1",
+ "unicode": "1F930-1F3FB",
+ "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
+ },
+ {
+ "name": "expecting_woman_tone1",
+ "unicode": "1F930-1F3FB",
+ "digest": "5a9f8ed2b631ecf8af111803a5c11f4c156435a5293cb50329c7b98697c8da25"
+ },
+ {
+ "name": "pregnant_woman_tone2",
+ "unicode": "1F930-1F3FC",
+ "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
+ },
+ {
+ "name": "expecting_woman_tone2",
+ "unicode": "1F930-1F3FC",
+ "digest": "279a2eafff603b11629c955b05f5bd3d7da9a271d4fb3f02e9ccd457b8d2d815"
+ },
+ {
+ "name": "pregnant_woman_tone3",
+ "unicode": "1F930-1F3FD",
+ "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
+ },
+ {
+ "name": "expecting_woman_tone3",
+ "unicode": "1F930-1F3FD",
+ "digest": "93bb63ec2312db315e3f0065520b715cc413ac0fd65538ec9b5cd97df2a42b20"
+ },
+ {
+ "name": "pregnant_woman_tone4",
+ "unicode": "1F930-1F3FE",
+ "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
+ },
+ {
+ "name": "expecting_woman_tone4",
+ "unicode": "1F930-1F3FE",
+ "digest": "b8dc3dcec894bfd832a249459b10850f8786b6778d8887a677d1291865623da2"
+ },
+ {
+ "name": "pregnant_woman_tone5",
+ "unicode": "1F930-1F3FF",
+ "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
+ },
+ {
+ "name": "expecting_woman_tone5",
+ "unicode": "1F930-1F3FF",
+ "digest": "73ee432752f81980f353a7f9b9f7a5ece62512dca08e15c1876b89227face21c"
+ },
+ {
+ "name": "prince",
+ "unicode": "1F934",
+ "digest": "34a0e0625f0a9825d3674192d6233b6cae4d8130451293df09f91a6a4165869c"
+ },
+ {
+ "name": "prince_tone1",
+ "unicode": "1F934-1F3FB",
+ "digest": "ccecdfeccb2ab1fceceae14f3fba875c8c7099785a4c40131c08a697b5b675fc"
+ },
+ {
+ "name": "prince_tone2",
+ "unicode": "1F934-1F3FC",
+ "digest": "c373fd3e0c1798415e3d8d88fab6c98c1bbdedcbe6f52f3a3899f6e2124a768d"
+ },
+ {
+ "name": "prince_tone3",
+ "unicode": "1F934-1F3FD",
+ "digest": "71d15695ca954d55aa69d3c753c7d31a8ba5329713a8ddbc90dafc11e524c4ef"
+ },
+ {
+ "name": "prince_tone4",
+ "unicode": "1F934-1F3FE",
+ "digest": "08f6cb32424f15cc3aaf83c31a5dac7c01a6be2f37ea8f13aed579ce6fb4db19"
+ },
+ {
+ "name": "prince_tone5",
+ "unicode": "1F934-1F3FF",
+ "digest": "77d521148efa33fa4d3409693d050fecfd948411e807327484f174e289834649"
+ },
+ {
"name": "princess",
"unicode": "1F478",
"digest": "efabd28480a843c735f0868734da2f9ce28133933b02ab07b645498f494f3f80"
@@ -8560,16 +8895,6 @@
"digest": "5e5307e3dc7ec4e16c9978fb00934c99c4adefca7d32732a244d1f2de71ce6f8"
},
{
- "name": "prohibited",
- "unicode": "1F6C7",
- "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f"
- },
- {
- "name": "prohibited_sign",
- "unicode": "1F6C7",
- "digest": "bc6cdea2269a0ec39576d98dc4cda2bd9efa4dc330dde870148c6a85ad9cc63f"
- },
- {
"name": "projector",
"unicode": "1F4FD",
"digest": "7f8e1fdb89584849a56ee34c62cab808af48b7bd4823467d090af4657a2e0420"
@@ -8625,11 +8950,6 @@
"digest": "c3f7d7008be6bab8dc02284d4d759abf7aafbb3dbbe3a53f0f5b2ff685af88f8"
},
{
- "name": "pushpin_black",
- "unicode": "1F588",
- "digest": "80ebac74edb9e8e1f8a219b32a676d318ed73b359cd8193b91b493d775307f63"
- },
- {
"name": "put_litter_in_its_place",
"unicode": "1F6AE",
"digest": "f52a57d6f1bada7b6e6b9a6458597d70cb701c01e1120d8cb1d7ff65e01d405c"
@@ -8710,6 +9030,66 @@
"digest": "a93aceb54e965f35e397e8c8716b1831614933308d026012d5464ee42783ed4d"
},
{
+ "name": "raised_back_of_hand",
+ "unicode": "1F91A",
+ "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
+ },
+ {
+ "name": "back_of_hand",
+ "unicode": "1F91A",
+ "digest": "20973a697e826625deba5ee3c4f25eb5e1737f2e860ac6fe4ee4d0e0c84b5e12"
+ },
+ {
+ "name": "raised_back_of_hand_tone1",
+ "unicode": "1F91A-1F3FB",
+ "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
+ },
+ {
+ "name": "back_of_hand_tone1",
+ "unicode": "1F91A-1F3FB",
+ "digest": "06af5941255ca69d10d99d0a512bbda6141a296453835dbccf259ce0afe1dd3d"
+ },
+ {
+ "name": "raised_back_of_hand_tone2",
+ "unicode": "1F91A-1F3FC",
+ "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
+ },
+ {
+ "name": "back_of_hand_tone2",
+ "unicode": "1F91A-1F3FC",
+ "digest": "429ed19555c9e5197b729b3e7bd8013346551051cb0b3fbc8a4372717c9a027d"
+ },
+ {
+ "name": "raised_back_of_hand_tone3",
+ "unicode": "1F91A-1F3FD",
+ "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
+ },
+ {
+ "name": "back_of_hand_tone3",
+ "unicode": "1F91A-1F3FD",
+ "digest": "487a1c3f19e77c99b520ec073de2acc4a9e585b739a84b3989f7de85d2c2045c"
+ },
+ {
+ "name": "raised_back_of_hand_tone4",
+ "unicode": "1F91A-1F3FE",
+ "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
+ },
+ {
+ "name": "back_of_hand_tone4",
+ "unicode": "1F91A-1F3FE",
+ "digest": "154254d8500c55ec3de698be4a352f9bcf06e2950cabc4eabaccad0f39a1e1e9"
+ },
+ {
+ "name": "raised_back_of_hand_tone5",
+ "unicode": "1F91A-1F3FF",
+ "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
+ },
+ {
+ "name": "back_of_hand_tone5",
+ "unicode": "1F91A-1F3FF",
+ "digest": "6e9c0855ecd5f14adca5e5862427c3d39ffcf86f7ddd3aaa1fefc3cefc7483c8"
+ },
+ {
"name": "raised_hand",
"unicode": "270B",
"digest": "5cf11be683aea985d5ba51fbd44722c2327311bfe26b61c3d441c90f5d5a195a"
@@ -8880,6 +9260,16 @@
"digest": "d20c918c1e528ff0947312738501ca9a6fb6ff4016aad07db7a8125d00fd65cd"
},
{
+ "name": "rhino",
+ "unicode": "1F98F",
+ "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
+ },
+ {
+ "name": "rhinoceros",
+ "unicode": "1F98F",
+ "digest": "163fa3acd78eead72c431a1f48b8465a6d45272a9169560e456d30b4df93dc6b"
+ },
+ {
"name": "ribbon",
"unicode": "1F380",
"digest": "74315fe907f9f0203afe139cd4552aa442eecfa2a64fac12db3e1292fc5a8828"
@@ -8905,29 +9295,64 @@
"digest": "b942a06d3da0570aca59bab0af57cd8c16863934f12a38f70339fd0a36f675f5"
},
{
- "name": "right_speaker",
- "unicode": "1F568",
- "digest": "d268bb84be863c0884620dfc6d2a764b0c7466d2f9810549b138e21ac70add4e"
+ "name": "right_facing_fist",
+ "unicode": "1F91C",
+ "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
+ },
+ {
+ "name": "right_fist",
+ "unicode": "1F91C",
+ "digest": "f815d1cc0c0345ddcc8886ae9c133582d7dc779732ac9b93dde1ab4fdd3b251d"
},
{
- "name": "right_speaker_one",
- "unicode": "1F569",
- "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375"
+ "name": "right_facing_fist_tone1",
+ "unicode": "1F91C-1F3FB",
+ "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
},
{
- "name": "right_speaker_with_one_sound_wave",
- "unicode": "1F569",
- "digest": "5b92daa87bdf6ee15e798bec382a2ee885f4e6e77a68a3f626adcfe4c782b375"
+ "name": "right_fist_tone1",
+ "unicode": "1F91C-1F3FB",
+ "digest": "0f9269b70cf68071d97389e059a2bdacffd73f2afd2ce6cfd7447bb1a4e9abbb"
},
{
- "name": "right_speaker_three",
- "unicode": "1F56A",
- "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80"
+ "name": "right_facing_fist_tone2",
+ "unicode": "1F91C-1F3FC",
+ "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
},
{
- "name": "right_speaker_with_three_sound_waves",
- "unicode": "1F56A",
- "digest": "4d00b720a65bd0f4c3682b290b1976ec2388d6ae61225398f4e70556ae9e5f80"
+ "name": "right_fist_tone2",
+ "unicode": "1F91C-1F3FC",
+ "digest": "32a9833db853972e49e65aa227fb0512c57362da190aa1cc40e1d64f238e837e"
+ },
+ {
+ "name": "right_facing_fist_tone3",
+ "unicode": "1F91C-1F3FD",
+ "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
+ },
+ {
+ "name": "right_fist_tone3",
+ "unicode": "1F91C-1F3FD",
+ "digest": "be4706f8bb088411f5cbbf9065a0ae5b773c97456bd975c2b6789765657847b9"
+ },
+ {
+ "name": "right_facing_fist_tone4",
+ "unicode": "1F91C-1F3FE",
+ "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
+ },
+ {
+ "name": "right_fist_tone4",
+ "unicode": "1F91C-1F3FE",
+ "digest": "1680862891a9d85c4b6f76232a80e2ef7428bcec93087c86eae2efaba9c6a3f7"
+ },
+ {
+ "name": "right_facing_fist_tone5",
+ "unicode": "1F91C-1F3FF",
+ "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
+ },
+ {
+ "name": "right_fist_tone5",
+ "unicode": "1F91C-1F3FF",
+ "digest": "388715a4bc2178c52bbb3bc2729f57be50acab5d751784c9f3220e86c6b1fbcc"
},
{
"name": "ring",
@@ -8935,11 +9360,6 @@
"digest": "b5322907222797b5e1786209cda88513e76cd397a40f0a7da24847245c95ef9d"
},
{
- "name": "ringing_bell",
- "unicode": "1F56D",
- "digest": "d71ab7fa937fc4af507b5b07ea58a4f31e875d9e8304ef2b850d7cebe0e9cd66"
- },
- {
"name": "robot",
"unicode": "1F916",
"digest": "4d788e6ec89279588b036fca6b17f5a981291681df8f90306ecf5c039de40848"
@@ -8955,6 +9375,16 @@
"digest": "b82e68a95aa89a6de344d6e256fef86a848ebc91de560b043b3e1f7fd072d57d"
},
{
+ "name": "rofl",
+ "unicode": "1F923",
+ "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
+ },
+ {
+ "name": "rolling_on_the_floor_laughing",
+ "unicode": "1F923",
+ "digest": "f4f99ba2ac67b97338f904f9384ff03fb832a2e427bf6e74611bf5fee45f1f48"
+ },
+ {
"name": "roller_coaster",
"unicode": "1F3A2",
"digest": "a65e9ace1d7900499777af1225995f17af90a398bb414764c20b6e09a8c23a2c"
@@ -8985,11 +9415,6 @@
"digest": "2537def4deef422d4e669b28b1a0675259306ab38601019df3ec3482b14e52d5"
},
{
- "name": "rosette_black",
- "unicode": "1F3F6",
- "digest": "ae8675891c88f9d98463d35178445950c39b0deb0f0e8b3f341228a6e0d0e477"
- },
- {
"name": "rotating_light",
"unicode": "1F6A8",
"digest": "91fcdb85a752ae904d335a978c7e7936aed4c75d414b35219b5a74430e51555f"
@@ -9090,6 +9515,16 @@
"digest": "0a786075f3d9da48ae91afccf6ae0d097888da9509d354ee1d3cb99afcc88fe4"
},
{
+ "name": "salad",
+ "unicode": "1F957",
+ "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
+ },
+ {
+ "name": "green_salad",
+ "unicode": "1F957",
+ "digest": "fe321487ab847abe670e68a83f1d9e096129741c689c769ee7de4a65aeac29f8"
+ },
+ {
"name": "sandal",
"unicode": "1F461",
"digest": "03c3077cb4bd900934f9bdf921165b465e5cc9a6bee53e45a091411bceb8892d"
@@ -9160,6 +9595,11 @@
"digest": "95225be28f05d8b5a6b6e6bf58d973f61f183ad4fef55a558dc1b810796b85c8"
},
{
+ "name": "scooter",
+ "unicode": "1F6F4",
+ "digest": "4a7db148880398db75e059711cb53edefb6b8fa9d442009f52856b887ab1dde4"
+ },
+ {
"name": "scorpion",
"unicode": "1F982",
"digest": "d41119d1ea5daf727c17dbea7dadec1718c72fc9f98ae88252161df5fde0938a"
@@ -9190,6 +9630,16 @@
"digest": "ae68d86fc2a07cae332451b23bd1ceba3f6526a6c56d8c1089777fa4632850e1"
},
{
+ "name": "second_place",
+ "unicode": "1F948",
+ "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
+ },
+ {
+ "name": "second_place_medal",
+ "unicode": "1F948",
+ "digest": "9e2336fc16e532829b55380252f94655b58817d47c909fc2570002c5b06b9c40"
+ },
+ {
"name": "secret",
"unicode": "3299",
"digest": "1d0b9adde2657f41421b135962de20820cf4b4eb0204044f9859522ab9d211b0"
@@ -9205,16 +9655,61 @@
"digest": "c0ec5e6d20e1afdc4e78eeddb1301c8b708ad6278e7287a4e4e825417c858e75"
},
{
+ "name": "selfie",
+ "unicode": "1F933",
+ "digest": "2a1bc9f18ad4d6fb893d91c88ef1b2d9bd063dc2bb1a4b08c248c30f52545d4e"
+ },
+ {
+ "name": "selfie_tone1",
+ "unicode": "1F933-1F3FB",
+ "digest": "26dc212ffed30c276bd6a66a72bc4513e68098a2205fb4ca5b51ccfa1de5b544"
+ },
+ {
+ "name": "selfie_tone2",
+ "unicode": "1F933-1F3FC",
+ "digest": "71eceaefda46e3521f374f76693e7fa8f215067498067900080e2925ca94d7de"
+ },
+ {
+ "name": "selfie_tone3",
+ "unicode": "1F933-1F3FD",
+ "digest": "53eabbd4f6b8ebbd2f7af7bf5cd64309c4039ac1c5b2180290a547deaafcebdf"
+ },
+ {
+ "name": "selfie_tone4",
+ "unicode": "1F933-1F3FE",
+ "digest": "0baad378b09652b99c5d458db2e03b4db14a1557db4ea0969806a0ca1d33d40c"
+ },
+ {
+ "name": "selfie_tone5",
+ "unicode": "1F933-1F3FF",
+ "digest": "9a07608f34ec4dad48764a855f83f3965709d7b2fd2342e6dc9ed61f23f4adfd"
+ },
+ {
"name": "seven",
"unicode": "0037-20E3",
"digest": "ae85172d2c76c44afb4e3b45d277d400abb2dc895244b9abfbd1dac1cd7c53c2"
},
{
+ "name": "shallow_pan_of_food",
+ "unicode": "1F958",
+ "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
+ },
+ {
+ "name": "paella",
+ "unicode": "1F958",
+ "digest": "7c7ad9d5d3f7226427d310b5853e8257fad899febe58dcbc5adb4677964f5c6d"
+ },
+ {
"name": "shamrock",
"unicode": "2618",
"digest": "68ed70c26e04a818439a1742d2da6bc169edd02db86b6e6f8014b651f3235488"
},
{
+ "name": "shark",
+ "unicode": "1F988",
+ "digest": "23a2364b6356e7bbb84c138e9cf58e2c68cd8caabb337a0c4d365ce87bf5d2da"
+ },
+ {
"name": "shaved_ice",
"unicode": "1F367",
"digest": "54048e77268b7548d03088517bf8558d11324db901ca57f9bec93f1873663a74"
@@ -9255,11 +9750,56 @@
"digest": "95a3f03c675207bb1354270d02a630c204455c47b3edca23c48523a40cf3ea3b"
},
{
+ "name": "shopping_cart",
+ "unicode": "1F6D2",
+ "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
+ },
+ {
+ "name": "shopping_trolley",
+ "unicode": "1F6D2",
+ "digest": "4599b63f6861cdb4d8272cac84435c24c1d4d6a73c66d51e04a1cd14a1d333e6"
+ },
+ {
"name": "shower",
"unicode": "1F6BF",
"digest": "6b3c767c0eb472d4861c6c3cc2735a5e2c09681872ef42a11dc89f3c80b9da01"
},
{
+ "name": "shrimp",
+ "unicode": "1F990",
+ "digest": "b3651f3be3767125076a013fe903854f5b456a8afae865cb219cf528e0f44caa"
+ },
+ {
+ "name": "shrug",
+ "unicode": "1F937",
+ "digest": "6e264243cc3b6e396069dea4357a958bdcd4081cb1af0ed6aa47235bef88cf27"
+ },
+ {
+ "name": "shrug_tone1",
+ "unicode": "1F937-1F3FB",
+ "digest": "0567b9fd95c8a857914003a5465a500ca79c8111811d45b865021b1b1d92d0b1"
+ },
+ {
+ "name": "shrug_tone2",
+ "unicode": "1F937-1F3FC",
+ "digest": "1557c2f5e3d4599c806d74c0b78afcca940678787534b6862bb89a20601bac8a"
+ },
+ {
+ "name": "shrug_tone3",
+ "unicode": "1F937-1F3FD",
+ "digest": "f02754541a7bf74ba7eebe6c27daf1e3e1dac25172c35b8ba45641e278dfda3d"
+ },
+ {
+ "name": "shrug_tone4",
+ "unicode": "1F937-1F3FE",
+ "digest": "2b5121164cb5f4e253d8fb31f6445cf8afaf30dba41732edc511440cdb78d15c"
+ },
+ {
+ "name": "shrug_tone5",
+ "unicode": "1F937-1F3FF",
+ "digest": "62d99a26bbad479f574f66208c41b9960cd41fb9d79d3a13fbdaa44682077115"
+ },
+ {
"name": "signal_strength",
"unicode": "1F4F6",
"digest": "2c6f04ba4ecd2d2d423e19eb52cfbfd253f4db6e0908d91c1af4ea6192597447"
@@ -9415,6 +9955,16 @@
"digest": "18da2d97c771149ef5454dd23470e900903a62ab93f9e2ce301aad5a8181d773"
},
{
+ "name": "sneezing_face",
+ "unicode": "1F927",
+ "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
+ },
+ {
+ "name": "sneeze",
+ "unicode": "1F927",
+ "digest": "c20ef571dc7e35572fe3c18b7845aefc89af083ea925c48a29de3b7387af6e17"
+ },
+ {
"name": "snowboarder",
"unicode": "1F3C2",
"digest": "c6e074139b851aa53b1ba6464d84da14b3da7412fc44c6c196a8469d76915c19"
@@ -9520,46 +10070,6 @@
"digest": "817100d9979456e7d2f253ac22e13b7a2302dc1590566214915b003e403c53ca"
},
{
- "name": "speech_left",
- "unicode": "1F5E8",
- "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
- },
- {
- "name": "left_speech_bubble",
- "unicode": "1F5E8",
- "digest": "912797107d574f5665411498b6e349dbdec69846f085b6dc356548c4155e90b0"
- },
- {
- "name": "speech_right",
- "unicode": "1F5E9",
- "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a"
- },
- {
- "name": "right_speech_bubble",
- "unicode": "1F5E9",
- "digest": "8439b13779163c15e678a78b08ebeeb7d131632df21d2a7868de7fed38ca9d8a"
- },
- {
- "name": "speech_three",
- "unicode": "1F5EB",
- "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9"
- },
- {
- "name": "three_speech_bubbles",
- "unicode": "1F5EB",
- "digest": "55a934f3659b6e75fdce0d0c4e2ea56dd34a43892c85a6666bd1882a0bfb92a9"
- },
- {
- "name": "speech_two",
- "unicode": "1F5EA",
- "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983"
- },
- {
- "name": "two_speech_bubbles",
- "unicode": "1F5EA",
- "digest": "0563ef0591da243673cf877462acc5d8e1d980a56e81668ac627de74d0c33983"
- },
- {
"name": "speedboat",
"unicode": "1F6A4",
"digest": "a523b2320f0b24be1e9fdbc1ff828e28d8fd9a64d51e5888ab453ef0bc9f0576"
@@ -9575,6 +10085,11 @@
"digest": "2434bdfbe56dcc4a43699dd59b638af431486b52fb1d6d685451f3b231b2be23"
},
{
+ "name": "spoon",
+ "unicode": "1F944",
+ "digest": "4fa31d59e5bffd2c45a8e01fcd5652e78a5691cbfa744e69882bc67173ddea05"
+ },
+ {
"name": "spy",
"unicode": "1F575",
"digest": "99fe3cdeff934726ee5855b0e401bf32570084aaad4eb10df837fd410ca742aa"
@@ -9635,6 +10150,11 @@
"digest": "ffc6fefd9a537124ebf0a9ddf387414dce1291335026064644f6cf9315591129"
},
{
+ "name": "squid",
+ "unicode": "1F991",
+ "digest": "65a1b318c2c506b9d26cfd8282a5cf9922109595c8d12e92c3f7481ac7c08c49"
+ },
+ {
"name": "stadium",
"unicode": "1F3DF",
"digest": "73bf955e767ba1518c9c92b2ba59a2aa1ec4b018652dffd97bcd74832a33789f"
@@ -9680,26 +10200,11 @@
"digest": "52ad0073f37b978faf3884fb193046f2b0614e1557bbcc9de1b020e42aff2dba"
},
{
- "name": "stereo",
- "unicode": "1F4FE",
- "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5"
- },
- {
- "name": "portable_stereo",
- "unicode": "1F4FE",
- "digest": "1ce1f9a83867514b8351ad4fd80c46bba04ad67dfb9874e63d7296e1a21161a5"
- },
- {
"name": "stew",
"unicode": "1F372",
"digest": "c16f61236db314ad8d9f2dd241ec1e15c8d64e5872cce93ec4d0996490dd39df"
},
{
- "name": "stock_chart",
- "unicode": "1F5E0",
- "digest": "4a0fbf54d19b0b5626f91c932a24e6ac12a65b4fc276d852ff4356c8c579d28a"
- },
- {
"name": "stop_button",
"unicode": "23F9",
"digest": "83f9d0da3ad845fef41b4e8336815d30e9c8f042ab2a8340894ade2f428fc98a"
@@ -9735,6 +10240,16 @@
"digest": "dbacd6428a2a2933212e6a4dc0c7f302177fb23b963626ccb26f27f91737f03d"
},
{
+ "name": "stuffed_flatbread",
+ "unicode": "1F959",
+ "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
+ },
+ {
+ "name": "stuffed_pita",
+ "unicode": "1F959",
+ "digest": "9f841f2520640d69be4f20a3199023d5811842b28556b5e1152e5ec11f0fda07"
+ },
+ {
"name": "sun_with_face",
"unicode": "1F31E",
"digest": "7256ff5263006c64c03f1eb66e3ddb56d67d785d65dacc37aa886d0cd4be63be"
@@ -9910,31 +10425,11 @@
"digest": "3a53851e641f8ad938ce3597b1afca2ea63c9314ff81f62563b99937496a13d7"
},
{
- "name": "telephone_black",
- "unicode": "1F57F",
- "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db"
- },
- {
- "name": "black_touchtone_telephone",
- "unicode": "1F57F",
- "digest": "c3a42a653a91d90c6b668f678419d5438f2e546050914b841623e57107e805db"
- },
- {
"name": "telephone_receiver",
"unicode": "1F4DE",
"digest": "1614d67f3d8814b0d75f39d55f9149e4d28ef57b343498625e62fcfff8365046"
},
{
- "name": "telephone_white",
- "unicode": "1F57E",
- "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581"
- },
- {
- "name": "white_touchtone_telephone",
- "unicode": "1F57E",
- "digest": "62a7e0e50c53e9f85eba51a92882e6064be05997910d3f7700e1e957dbaf0581"
- },
- {
"name": "telescope",
"unicode": "1F52D",
"digest": "4adf40387870276c4f59fb050d441023e8dac784365b6a8c0282fb519780b495"
@@ -9980,29 +10475,19 @@
"digest": "4f0b84e5ab8a650cafb166e93688f0e9b31b9ade22a91035261ac90490edb9d3"
},
{
- "name": "thought_balloon",
- "unicode": "1F4AD",
- "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e"
- },
- {
- "name": "thought_left",
- "unicode": "1F5EC",
- "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46"
+ "name": "third_place",
+ "unicode": "1F949",
+ "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
},
{
- "name": "left_thought_bubble",
- "unicode": "1F5EC",
- "digest": "4fd591bf4318df73d1b17f434a449d8e95f49cca53a3d8f4d1ca983f3809ef46"
+ "name": "third_place_medal",
+ "unicode": "1F949",
+ "digest": "27c9bcba44ad95bee30882cc0722e8b0a798206306655dd648e884447ed26808"
},
{
- "name": "thought_right",
- "unicode": "1F5ED",
- "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae"
- },
- {
- "name": "right_thought_bubble",
- "unicode": "1F5ED",
- "digest": "0e8c0ce26e2d0e30894f5394b0736456e8268f775e0e7eda4c7dc3c2ff9231ae"
+ "name": "thought_balloon",
+ "unicode": "1F4AD",
+ "digest": "bf59624560c333561d636aedf2c8827089e275895cf434974daaabb3d5cea46e"
},
{
"name": "three",
@@ -10010,26 +10495,6 @@
"digest": "d3f85828787799c769655c38a519cad0743ab799ab276c7606e6e6894cc442e6"
},
{
- "name": "thumbs_down_reverse",
- "unicode": "1F593",
- "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958"
- },
- {
- "name": "reversed_thumbs_down_sign",
- "unicode": "1F593",
- "digest": "a8b561e389bc4e4b07fba70994f6445e5ddc6afe68922fcb6e9e7282d19ad958"
- },
- {
- "name": "thumbs_up_reverse",
- "unicode": "1F592",
- "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837"
- },
- {
- "name": "reversed_thumbs_up_sign",
- "unicode": "1F592",
- "digest": "b6e52715c5ce590bfd08f6e05058ec3765ea2da341b11f9825d100608b173837"
- },
- {
"name": "thumbsdown",
"unicode": "1F44E",
"digest": "5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61"
@@ -10315,31 +10780,11 @@
"digest": "06e65d549e771632f3c64287a38ba67236f9800ccb6a23c3b592bc010e24e122"
},
{
- "name": "train_diesel",
- "unicode": "1F6F2",
- "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c"
- },
- {
- "name": "diesel_locomotive",
- "unicode": "1F6F2",
- "digest": "621bb967cd93fa9f8fd4b155965cc7572d3f91f88d94938ba10c8626718b623c"
- },
- {
"name": "tram",
"unicode": "1F68A",
"digest": "21a7699f1a94f06dcb4d1e896448b98a4205f8efe902a8ac169a5005d11ab100"
},
{
- "name": "triangle_round",
- "unicode": "1F6C6",
- "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5"
- },
- {
- "name": "triangle_with_rounded_corners",
- "unicode": "1F6C6",
- "digest": "e24bb39ecfaaa746b03dc8418697d09ef327d5b077db39014f39d5fb87e23bd5"
- },
- {
"name": "triangular_flag_on_post",
"unicode": "1F6A9",
"digest": "1f5ce3828a42f5b1717bac1521d0502cf7081ad9f15e8ed292c1a65f0d1386da"
@@ -10395,19 +10840,19 @@
"digest": "e744e8dbbdc6b126bd5b15aad56b524191de5a604189f4ab6d96730dfef4d086"
},
{
- "name": "turkey",
- "unicode": "1F983",
- "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4"
+ "name": "tumbler_glass",
+ "unicode": "1F943",
+ "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
},
{
- "name": "turned_ok_hand",
- "unicode": "1F58F",
- "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246"
+ "name": "whisky",
+ "unicode": "1F943",
+ "digest": "7a38658274b9ff28836725a1dbfad49b8fa3af5ec8385e629db6bfdc7d93907a"
},
{
- "name": "turned_ok_hand_sign",
- "unicode": "1F58F",
- "digest": "8a6c5b7d4c737866e7e32c6d9f7f447a48a0ac57a8909d43f87367d4a9b59246"
+ "name": "turkey",
+ "unicode": "1F983",
+ "digest": "bf5daef15716b66636a5fdb6d059420521443c0603e2d56bd7c99c791a7285f4"
},
{
"name": "turtle",
@@ -10760,6 +11205,36 @@
"digest": "ba6a840d4f57f8f9f3e9f29b8a030faf02a3a3d912e3e31b067616b2ac48a3d1"
},
{
+ "name": "water_polo",
+ "unicode": "1F93D",
+ "digest": "fc77e1d2a84a9f4cf0cf19c1ea10cf137cf0940b9103a523121eda87677ad148"
+ },
+ {
+ "name": "water_polo_tone1",
+ "unicode": "1F93D-1F3FB",
+ "digest": "3be28384edd29ada8109f07720d601a9d5866ed63e6234efe9ee1a194ed5d0c5"
+ },
+ {
+ "name": "water_polo_tone2",
+ "unicode": "1F93D-1F3FC",
+ "digest": "afcd3f28c6719f869ca79a6fd1ccade2ea976ade844fbc1081fc72865bcb652f"
+ },
+ {
+ "name": "water_polo_tone3",
+ "unicode": "1F93D-1F3FD",
+ "digest": "d19481c9b82d9413e99c2652e020fd763f2b54408dedaffec8dfe80973ded407"
+ },
+ {
+ "name": "water_polo_tone4",
+ "unicode": "1F93D-1F3FE",
+ "digest": "375972d882b627e8d525e632e58b30346fc3e01858d7d08d62a9d3bf8132bbc7"
+ },
+ {
+ "name": "water_polo_tone5",
+ "unicode": "1F93D-1F3FF",
+ "digest": "a8e1ced1c5382a8147a1d1801a133cada9a0e52e41de6272e56c3c1f426f6048"
+ },
+ {
"name": "watermelon",
"unicode": "1F349",
"digest": "42a3821d2e4dd595c93f5db7a5c70b7af486b8f0ddd3b9d26bc4e743a88e699a"
@@ -10915,6 +11390,16 @@
"digest": "0a6164cdadf2413555b7ef47b95f823f5a010f36d2dacfb1a38335a0f59e9601"
},
{
+ "name": "wilted_rose",
+ "unicode": "1F940",
+ "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
+ },
+ {
+ "name": "wilted_flower",
+ "unicode": "1F940",
+ "digest": "2c9e01ab9a61d057c71478b09ba7d82ae08f4a5a1c2212b7ad562b74f616677f"
+ },
+ {
"name": "wind_blowing_face",
"unicode": "1F32C",
"digest": "e4f63149cbc8829118571f6a93487b96d26665fc15d17d578cca4e5c752cd54f"
@@ -10995,14 +11480,69 @@
"digest": "81aae53bc892035b905bf3ec5b442a8ecc95027c5fa9eb51b7c3e7d8fad3f3f4"
},
{
- "name": "writing_hand",
- "unicode": "1F58E",
- "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb"
+ "name": "wrestlers",
+ "unicode": "1F93C",
+ "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
+ },
+ {
+ "name": "wrestling",
+ "unicode": "1F93C",
+ "digest": "9be983f3f9438f3ab8f6b643a958371d1e710c6d78e728f3465141811f05c2d5"
},
{
- "name": "left_writing_hand",
- "unicode": "1F58E",
- "digest": "c4fc18ece6778339ebe14438aaf570e22385c3010c2d341824fa72ac6068cfeb"
+ "name": "wrestlers_tone1",
+ "unicode": "1F93C-1F3FB",
+ "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
+ },
+ {
+ "name": "wrestling_tone1",
+ "unicode": "1F93C-1F3FB",
+ "digest": "60461f83bfc93ce59dd027eab4782b7f206a7b142719fa72f301e047dc83a5d9"
+ },
+ {
+ "name": "wrestlers_tone2",
+ "unicode": "1F93C-1F3FC",
+ "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
+ },
+ {
+ "name": "wrestling_tone2",
+ "unicode": "1F93C-1F3FC",
+ "digest": "67ad93c86e6c58d552c18e7a0105cc81fd9bb0474da51f788eba2e4c14b4a636"
+ },
+ {
+ "name": "wrestlers_tone3",
+ "unicode": "1F93C-1F3FD",
+ "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
+ },
+ {
+ "name": "wrestling_tone3",
+ "unicode": "1F93C-1F3FD",
+ "digest": "6bfd06c4435cabf2def153912040e05bf8db424fa383148ddda6d0ce8a8a3349"
+ },
+ {
+ "name": "wrestlers_tone4",
+ "unicode": "1F93C-1F3FE",
+ "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
+ },
+ {
+ "name": "wrestling_tone4",
+ "unicode": "1F93C-1F3FE",
+ "digest": "597312678834c4d288c238482879856d5eba4620deb1eaef495f428e2ba5f2a5"
+ },
+ {
+ "name": "wrestlers_tone5",
+ "unicode": "1F93C-1F3FF",
+ "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
+ },
+ {
+ "name": "wrestling_tone5",
+ "unicode": "1F93C-1F3FF",
+ "digest": "d6aebdf1e44fd825b9a5b3716aefbc53f4b4dbb73cb2a628c0f2994ebfd34614"
+ },
+ {
+ "name": "writing_hand",
+ "unicode": "270D",
+ "digest": "110517ae4da5587e8b0662881658e27da4120bfacec54734fd6657831d4d782f"
},
{
"name": "writing_hand_tone1",
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
index 7f204c1a8e0..2a990913b9c 100644
--- a/fixtures/emojis/index.json
+++ b/fixtures/emojis/index.json
@@ -4,7 +4,7 @@
"unicode_alternates": [],
"name": "hundred points symbol",
"shortname": ":100:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15,12 +15,14 @@
"percent",
"a",
"plus",
- "perfect",
"school",
"quiz",
- "score",
"test",
- "exam"
+ "exam",
+ "symbol",
+ "wow",
+ "win",
+ "parties"
],
"moji": "💯"
},
@@ -29,12 +31,13 @@
"unicode_alternates": [],
"name": "input symbol for numbers",
"shortname": ":1234:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "numbers"
+ "numbers",
+ "symbol"
],
"moji": "🔢"
},
@@ -43,16 +46,20 @@
"unicode_alternates": [],
"name": "billiards",
"shortname": ":8ball:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"pool",
"billiards",
"eight ball",
- "pool",
"pocket ball",
- "cue"
+ "cue",
+ "game",
+ "ball",
+ "sport",
+ "luck",
+ "boys night"
],
"moji": "🎱"
},
@@ -61,13 +68,14 @@
"unicode_alternates": [],
"name": "negative squared latin capital letter a",
"shortname": ":a:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"letter",
- "red-square"
+ "red-square",
+ "symbol"
],
"moji": "🅰"
},
@@ -76,12 +84,13 @@
"unicode_alternates": [],
"name": "negative squared ab",
"shortname": ":ab:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
- "red-square"
+ "red-square",
+ "symbol"
],
"moji": "🆎"
},
@@ -90,12 +99,13 @@
"unicode_alternates": [],
"name": "input symbol for latin letters",
"shortname": ":abc:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "🔤"
},
@@ -104,12 +114,13 @@
"unicode_alternates": [],
"name": "input symbol for latin small letters",
"shortname": ":abcd:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "🔡"
},
@@ -118,7 +129,7 @@
"unicode_alternates": [],
"name": "circled ideograph accept",
"shortname": ":accept:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -127,7 +138,8 @@
"good",
"kanji",
"ok",
- "yes"
+ "yes",
+ "symbol"
],
"moji": "🉑"
},
@@ -136,7 +148,7 @@
"unicode_alternates": [],
"name": "aerial tramway",
"shortname": ":aerial_tramway:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -146,7 +158,9 @@
"tram",
"tramway",
"cable",
- "transport"
+ "transport",
+ "travel",
+ "train"
],
"moji": "🚡"
},
@@ -157,7 +171,7 @@
],
"name": "airplane",
"shortname": ":airplane:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -173,7 +187,8 @@
"jet",
"jumbo",
"boeing",
- "airbus"
+ "airbus",
+ "vacation"
],
"moji": "✈"
},
@@ -182,7 +197,7 @@
"unicode_alternates": [],
"name": "airplane arriving",
"shortname": ":airplane_arriving:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -197,15 +212,17 @@
"jet",
"jumbo",
"boeing",
- "airbus"
- ]
+ "airbus",
+ "vacation"
+ ],
+ "moji": "🛬"
},
"airplane_departure": {
"unicode": "1F6EB",
"unicode_alternates": [],
"name": "airplane departure",
"shortname": ":airplane_departure:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -221,30 +238,17 @@
"jumbo",
"boeing",
"airbus",
- "leaving"
- ]
- },
- "airplane_northeast": {
- "unicode": "1F6EA",
- "unicode_alternates": [],
- "name": "northeast-pointing airplane",
- "shortname": ":airplane_northeast:",
- "category": "travel_places",
- "aliases": [
- ":northeast_pointing_airplane:"
+ "leaving",
+ "vacation"
],
- "aliases_ascii": [],
- "keywords": [
- "plane",
- "travel"
- ]
+ "moji": "🛫"
},
"airplane_small": {
"unicode": "1F6E9",
"unicode_alternates": [],
"name": "small airplane",
"shortname": ":airplane_small:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":small_airplane:"
],
@@ -261,38 +265,10 @@
"jet",
"jumbo",
"boeing",
- "airbus"
- ]
- },
- "airplane_small_up": {
- "unicode": "1F6E8",
- "unicode_alternates": [],
- "name": "up-pointing small airplane",
- "shortname": ":airplane_small_up:",
- "category": "travel_places",
- "aliases": [
- ":up_pointing_small_airplane:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "plane",
- "travel"
- ]
- },
- "airplane_up": {
- "unicode": "1F6E7",
- "unicode_alternates": [],
- "name": "up-pointing airplane",
- "shortname": ":airplane_up:",
- "category": "travel_places",
- "aliases": [
- ":up_pointing_airplane:"
+ "airbus",
+ "vacation"
],
- "aliases_ascii": [],
- "keywords": [
- "plane",
- "travel"
- ]
+ "moji": "🛩"
},
"alarm_clock": {
"unicode": "23F0",
@@ -304,13 +280,14 @@
"aliases_ascii": [],
"keywords": [
"time",
- "wake"
+ "wake",
+ "object"
],
"moji": "⏰"
},
"alembic": {
"unicode": "2697",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "alembic",
"shortname": ":alembic:",
"category": "objects",
@@ -319,22 +296,27 @@
"keywords": [
"chemistry",
"object",
- "tool"
- ]
+ "tool",
+ "science"
+ ],
+ "moji": "⚗"
},
"alien": {
"unicode": "1F47D",
"unicode_alternates": [],
"name": "extraterrestrial alien",
"shortname": ":alien:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"UFO",
"paul",
"alien",
- "ufo"
+ "ufo",
+ "space",
+ "monster",
+ "scientology"
],
"moji": "👽"
},
@@ -343,7 +325,7 @@
"unicode_alternates": [],
"name": "ambulance",
"shortname": ":ambulance:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -353,19 +335,23 @@
"emergency",
"medical",
"help",
- "assistance"
+ "assistance",
+ "transportation"
],
"moji": "🚑"
},
"amphora": {
"unicode": "1F3FA",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "amphora",
"shortname": ":amphora:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "object"
+ ],
+ "moji": "🏺"
},
"anchor": {
"unicode": "2693",
@@ -374,21 +360,23 @@
],
"name": "anchor",
"shortname": ":anchor:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"ferry",
"ship",
"anchor",
- "ship",
"boat",
"ocean",
"harbor",
"marina",
"shipyard",
"sailor",
- "tattoo"
+ "tattoo",
+ "object",
+ "travel",
+ "vacation"
],
"moji": "⚓"
},
@@ -397,7 +385,7 @@
"unicode_alternates": [],
"name": "baby angel",
"shortname": ":angel:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -406,16 +394,17 @@
"halo",
"cupid",
"wings",
- "halo",
"heaven",
- "wings",
- "jesus"
+ "jesus",
+ "people",
+ "diversity",
+ "omg"
],
"moji": "👼"
},
"angel_tone1": {
"unicode": "1F47C-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby angel tone 1",
"shortname": ":angel_tone1:",
"category": "people",
@@ -427,11 +416,12 @@
"heaven",
"wings",
"jesus"
- ]
+ ],
+ "moji": "👼🏻"
},
"angel_tone2": {
"unicode": "1F47C-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby angel tone 2",
"shortname": ":angel_tone2:",
"category": "people",
@@ -443,11 +433,12 @@
"heaven",
"wings",
"jesus"
- ]
+ ],
+ "moji": "👼🏼"
},
"angel_tone3": {
"unicode": "1F47C-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby angel tone 3",
"shortname": ":angel_tone3:",
"category": "people",
@@ -459,11 +450,12 @@
"heaven",
"wings",
"jesus"
- ]
+ ],
+ "moji": "👼🏽"
},
"angel_tone4": {
"unicode": "1F47C-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby angel tone 4",
"shortname": ":angel_tone4:",
"category": "people",
@@ -475,11 +467,12 @@
"heaven",
"wings",
"jesus"
- ]
+ ],
+ "moji": "👼🏾"
},
"angel_tone5": {
"unicode": "1F47C-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby angel tone 5",
"shortname": ":angel_tone5:",
"category": "people",
@@ -491,50 +484,31 @@
"heaven",
"wings",
"jesus"
- ]
+ ],
+ "moji": "👼🏿"
},
"anger": {
"unicode": "1F4A2",
"unicode_alternates": [],
"name": "anger symbol",
"shortname": ":anger:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"anger",
"angry",
- "mad"
+ "mad",
+ "symbol"
],
"moji": "💢"
},
- "anger_left": {
- "unicode": "1F5EE",
- "unicode_alternates": [],
- "name": "left anger bubble",
- "shortname": ":anger_left:",
- "category": "objects_symbols",
- "aliases": [
- ":left_anger_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "speech",
- "balloon",
- "talk",
- "mood",
- "conversation",
- "communication",
- "comic",
- "angry"
- ]
- },
"anger_right": {
"unicode": "1F5EF",
"unicode_alternates": [],
"name": "right anger bubble",
"shortname": ":anger_right:",
- "category": "objects_symbols",
+ "category": "symbols",
"aliases": [
":right_anger_bubble:"
],
@@ -547,15 +521,17 @@
"conversation",
"communication",
"comic",
- "angry"
- ]
+ "angry",
+ "symbol"
+ ],
+ "moji": "🗯"
},
"angry": {
"unicode": "1F620",
"unicode_alternates": [],
"name": "angry face",
"shortname": ":angry:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
">:(",
@@ -571,7 +547,8 @@
"annoyed",
"face",
"frustrated",
- "mad"
+ "smiley",
+ "emotion"
],
"moji": "😠"
},
@@ -580,7 +557,7 @@
"unicode_alternates": [],
"name": "anguished face",
"shortname": ":anguished:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -592,7 +569,11 @@
"ouch",
"misery",
"distress",
- "grief"
+ "grief",
+ "sad",
+ "smiley",
+ "surprised",
+ "emotion"
],
"moji": "😧"
},
@@ -609,8 +590,8 @@
"insect",
"ant",
"queen",
- "insect",
- "team"
+ "team",
+ "insects"
],
"moji": "🐜"
},
@@ -619,20 +600,21 @@
"unicode_alternates": [],
"name": "red apple",
"shortname": ":apple:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fruit",
"mac",
"apple",
- "fruit",
"electronics",
"red",
"doctor",
"teacher",
"school",
- "core"
+ "core",
+ "food",
+ "creationism"
],
"moji": "🍎"
},
@@ -643,7 +625,7 @@
],
"name": "aquarius",
"shortname": ":aquarius:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -657,9 +639,8 @@
"zodiac",
"sign",
"purple-square",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♒"
},
@@ -670,7 +651,7 @@
],
"name": "aries",
"shortname": ":aries:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -683,9 +664,8 @@
"zodiac",
"sign",
"purple-square",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♈"
},
@@ -696,12 +676,14 @@
],
"name": "black left-pointing triangle",
"shortname": ":arrow_backward:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol",
+ "triangle"
],
"moji": "◀"
},
@@ -710,12 +692,13 @@
"unicode_alternates": [],
"name": "black down-pointing double triangle",
"shortname": ":arrow_double_down:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "⏬"
},
@@ -724,12 +707,13 @@
"unicode_alternates": [],
"name": "black up-pointing double triangle",
"shortname": ":arrow_double_up:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "⏫"
},
@@ -740,12 +724,13 @@
],
"name": "downwards black arrow",
"shortname": ":arrow_down:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "⬇"
},
@@ -754,12 +739,14 @@
"unicode_alternates": [],
"name": "down-pointing small red triangle",
"shortname": ":arrow_down_small:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol",
+ "triangle"
],
"moji": "🔽"
},
@@ -770,12 +757,14 @@
],
"name": "black right-pointing triangle",
"shortname": ":arrow_forward:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol",
+ "triangle"
],
"moji": "▶"
},
@@ -786,12 +775,13 @@
],
"name": "arrow pointing rightwards then curving downwards",
"shortname": ":arrow_heading_down:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "⤵"
},
@@ -802,12 +792,13 @@
],
"name": "arrow pointing rightwards then curving upwards",
"shortname": ":arrow_heading_up:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "⤴"
},
@@ -818,13 +809,14 @@
],
"name": "leftwards black arrow",
"shortname": ":arrow_left:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
"blue-square",
- "previous"
+ "previous",
+ "symbol"
],
"moji": "⬅"
},
@@ -835,12 +827,13 @@
],
"name": "south west arrow",
"shortname": ":arrow_lower_left:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "↙"
},
@@ -851,12 +844,13 @@
],
"name": "south east arrow",
"shortname": ":arrow_lower_right:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "↘"
},
@@ -867,12 +861,14 @@
],
"name": "black rightwards arrow",
"shortname": ":arrow_right:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "next"
+ "next",
+ "arrow",
+ "symbol"
],
"moji": "➡"
},
@@ -883,11 +879,13 @@
],
"name": "rightwards arrow with hook",
"shortname": ":arrow_right_hook:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "↪"
},
@@ -898,11 +896,13 @@
],
"name": "upwards black arrow",
"shortname": ":arrow_up:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "⬆"
},
@@ -913,11 +913,13 @@
],
"name": "up down arrow",
"shortname": ":arrow_up_down:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "↕"
},
@@ -926,11 +928,14 @@
"unicode_alternates": [],
"name": "up-pointing small red triangle",
"shortname": ":arrow_up_small:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol",
+ "triangle"
],
"moji": "🔼"
},
@@ -941,11 +946,13 @@
],
"name": "north west arrow",
"shortname": ":arrow_upper_left:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "↖"
},
@@ -956,11 +963,13 @@
],
"name": "north east arrow",
"shortname": ":arrow_upper_right:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "↗"
},
@@ -969,11 +978,13 @@
"unicode_alternates": [],
"name": "clockwise downwards and upwards open circle arrows",
"shortname": ":arrows_clockwise:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "sync"
+ "sync",
+ "arrow",
+ "symbol"
],
"moji": "🔃"
},
@@ -982,12 +993,14 @@
"unicode_alternates": [],
"name": "anticlockwise downwards and upwards open circle ar",
"shortname": ":arrows_counterclockwise:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "sync"
+ "sync",
+ "arrow",
+ "symbol"
],
"moji": "🔄"
},
@@ -996,7 +1009,7 @@
"unicode_alternates": [],
"name": "artist palette",
"shortname": ":art:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1007,8 +1020,6 @@
"palette",
"art",
"colors",
- "paint",
- "draw",
"brush",
"pastels",
"oils"
@@ -1020,7 +1031,7 @@
"unicode_alternates": [],
"name": "articulated lorry",
"shortname": ":articulated_lorry:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1035,24 +1046,11 @@
],
"moji": "🚛"
},
- "ascending_notes": {
- "unicode": "1F39C",
- "unicode_alternates": [],
- "name": "beamed ascending musical notes",
- "shortname": ":ascending_notes:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "score",
- "music",
- "sound",
- "tone"
- ]
- },
"asterisk": {
"unicode": "002A-20E3",
- "unicode_alternates": "002a-fe0f-20e3",
+ "unicode_alternates": [
+ "002A-FE0F-20E3"
+ ],
"name": "keycap asterisk",
"shortname": ":asterisk:",
"category": "symbols",
@@ -1064,14 +1062,15 @@
"*",
"star",
"symbol"
- ]
+ ],
+ "moji": "*⃣"
},
"astonished": {
"unicode": "1F632",
"unicode_alternates": [],
"name": "astonished face",
"shortname": ":astonished:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1079,7 +1078,12 @@
"xox",
"shocked",
"surprise",
- "astonished"
+ "astonished",
+ "smiley",
+ "surprised",
+ "wow",
+ "emotion",
+ "omg"
],
"moji": "😲"
},
@@ -1088,12 +1092,16 @@
"unicode_alternates": [],
"name": "athletic shoe",
"shortname": ":athletic_shoe:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"shoes",
- "sports"
+ "sports",
+ "fashion",
+ "shoe",
+ "accessories",
+ "boys night"
],
"moji": "👟"
},
@@ -1102,7 +1110,7 @@
"unicode_alternates": [],
"name": "automated teller machine",
"shortname": ":atm:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1115,17 +1123,16 @@
"bank",
"adam",
"payday",
- "bank",
"blue-square",
- "cash",
- "money",
- "payment"
+ "payment",
+ "electronics",
+ "symbol"
],
"moji": "🏧"
},
"atom": {
"unicode": "269B",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "atom symbol",
"shortname": ":atom:",
"category": "symbols",
@@ -1134,21 +1141,36 @@
],
"aliases_ascii": [],
"keywords": [
- "atheist"
- ]
+ "atheist",
+ "symbol",
+ "science"
+ ],
+ "moji": "⚛"
+ },
+ "avocado": {
+ "unicode": "1F951",
+ "unicode_alternates": [],
+ "name": "avocado",
+ "shortname": ":avocado:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥑"
},
"b": {
"unicode": "1F171",
"unicode_alternates": [],
"name": "negative squared latin capital letter b",
"shortname": ":b:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"letter",
- "red-square"
+ "red-square",
+ "symbol"
],
"moji": "🅱"
},
@@ -1157,13 +1179,16 @@
"unicode_alternates": [],
"name": "baby",
"shortname": ":baby:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"boy",
"child",
- "infant"
+ "infant",
+ "people",
+ "baby",
+ "diversity"
],
"moji": "👶"
},
@@ -1172,7 +1197,7 @@
"unicode_alternates": [],
"name": "baby bottle",
"shortname": ":baby_bottle:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1184,7 +1209,9 @@
"mother",
"nipple",
"newborn",
- "formula"
+ "formula",
+ "drink",
+ "object"
],
"moji": "🍼"
},
@@ -1202,7 +1229,6 @@
"chick",
"baby",
"bird",
- "chicken",
"young",
"woman",
"cute"
@@ -1214,7 +1240,7 @@
"unicode_alternates": [],
"name": "baby symbol",
"shortname": ":baby_symbol:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1226,13 +1252,14 @@
"human",
"diaper",
"small",
- "babe"
+ "babe",
+ "symbol"
],
"moji": "🚼"
},
"baby_tone1": {
"unicode": "1F476-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby tone 1",
"shortname": ":baby_tone1:",
"category": "people",
@@ -1242,11 +1269,12 @@
"child",
"infant",
"toddler"
- ]
+ ],
+ "moji": "👶🏻"
},
"baby_tone2": {
"unicode": "1F476-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby tone 2",
"shortname": ":baby_tone2:",
"category": "people",
@@ -1256,11 +1284,12 @@
"child",
"infant",
"toddler"
- ]
+ ],
+ "moji": "👶🏼"
},
"baby_tone3": {
"unicode": "1F476-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby tone 3",
"shortname": ":baby_tone3:",
"category": "people",
@@ -1270,11 +1299,12 @@
"child",
"infant",
"toddler"
- ]
+ ],
+ "moji": "👶🏽"
},
"baby_tone4": {
"unicode": "1F476-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby tone 4",
"shortname": ":baby_tone4:",
"category": "people",
@@ -1284,11 +1314,12 @@
"child",
"infant",
"toddler"
- ]
+ ],
+ "moji": "👶🏾"
},
"baby_tone5": {
"unicode": "1F476-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "baby tone 5",
"shortname": ":baby_tone5:",
"category": "people",
@@ -1298,37 +1329,57 @@
"child",
"infant",
"toddler"
- ]
+ ],
+ "moji": "👶🏿"
},
"back": {
"unicode": "1F519",
"unicode_alternates": [],
"name": "back with leftwards arrow above",
"shortname": ":back:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "arrow"
+ "arrow",
+ "symbol"
],
"moji": "🔙"
},
+ "bacon": {
+ "unicode": "1F953",
+ "unicode_alternates": [],
+ "name": "bacon",
+ "shortname": ":bacon:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "pig"
+ ],
+ "moji": "🥓"
+ },
"badminton": {
"unicode": "1F3F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "badminton racquet",
"shortname": ":badminton:",
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "game",
+ "sport",
+ "badminton"
+ ],
+ "moji": "🏸"
},
"baggage_claim": {
"unicode": "1F6C4",
"unicode_alternates": [],
"name": "baggage claim",
"shortname": ":baggage_claim:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1338,7 +1389,8 @@
"bag",
"baggage",
"luggage",
- "travel"
+ "travel",
+ "symbol"
],
"moji": "🛄"
},
@@ -1355,11 +1407,13 @@
"party",
"balloon",
"birthday",
- "celebration",
"helium",
"gas",
"children",
- "float"
+ "float",
+ "object",
+ "good",
+ "parties"
],
"moji": "🎈"
},
@@ -1368,29 +1422,17 @@
"unicode_alternates": [],
"name": "ballot box with ballot",
"shortname": ":ballot_box:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":ballot_box_with_ballot:"
],
"aliases_ascii": [],
"keywords": [
- "vote"
- ]
- },
- "ballot_box_check": {
- "unicode": "1F5F9",
- "unicode_alternates": [],
- "name": "ballot box with bold check",
- "shortname": ":ballot_box_check:",
- "category": "objects_symbols",
- "aliases": [
- ":ballot_box_with_bold_check:"
+ "vote",
+ "object",
+ "office"
],
- "aliases_ascii": [],
- "keywords": [
- "mark",
- "vote"
- ]
+ "moji": "🗳"
},
"ballot_box_with_check": {
"unicode": "2611",
@@ -1399,51 +1441,22 @@
],
"name": "ballot box with check",
"shortname": ":ballot_box_with_check:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"agree",
- "ok"
+ "ok",
+ "symbol"
],
"moji": "☑"
},
- "ballot_box_x": {
- "unicode": "1F5F5",
- "unicode_alternates": [],
- "name": "ballot box with script x",
- "shortname": ":ballot_box_x:",
- "category": "objects_symbols",
- "aliases": [
- ":ballot_box_with_script_x:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "mark",
- "vote"
- ]
- },
- "ballot_x": {
- "unicode": "1F5F4",
- "unicode_alternates": [],
- "name": "ballot script x",
- "shortname": ":ballot_x:",
- "category": "objects_symbols",
- "aliases": [
- ":ballot_script_x:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "mark",
- "vote"
- ]
- },
"bamboo": {
"unicode": "1F38D",
"unicode_alternates": [],
"name": "pine decoration",
"shortname": ":bamboo:",
- "category": "objects",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1472,7 +1485,7 @@
"unicode_alternates": [],
"name": "banana",
"shortname": ":banana:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1480,7 +1493,8 @@
"fruit",
"banana",
"peel",
- "bunch"
+ "bunch",
+ "penis"
],
"moji": "🍌"
},
@@ -1491,12 +1505,14 @@
],
"name": "double exclamation mark",
"shortname": ":bangbang:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"exclamation",
- "surprise"
+ "surprise",
+ "symbol",
+ "punctuation"
],
"moji": "‼"
},
@@ -1505,11 +1521,12 @@
"unicode_alternates": [],
"name": "bank",
"shortname": ":bank:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "building"
+ "building",
+ "places"
],
"moji": "🏦"
},
@@ -1524,7 +1541,9 @@
"keywords": [
"graph",
"presentation",
- "stats"
+ "stats",
+ "work",
+ "office"
],
"moji": "📊"
},
@@ -1533,13 +1552,14 @@
"unicode_alternates": [],
"name": "barber pole",
"shortname": ":barber:",
- "category": "places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"hair",
"salon",
- "style"
+ "style",
+ "object"
],
"moji": "💈"
},
@@ -1550,13 +1570,17 @@
],
"name": "baseball",
"shortname": ":baseball:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"MLB",
"balls",
- "sports"
+ "sports",
+ "game",
+ "ball",
+ "sport",
+ "baseball"
],
"moji": "⚾"
},
@@ -1565,7 +1589,7 @@
"unicode_alternates": [],
"name": "basketball and hoop",
"shortname": ":basketball:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1578,13 +1602,16 @@
"hoop",
"net",
"swish",
- "rip city"
+ "rip city",
+ "game",
+ "ball",
+ "sport"
],
"moji": "🏀"
},
"basketball_player": {
"unicode": "26F9",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball",
"shortname": ":basketball_player:",
"category": "activity",
@@ -1594,12 +1621,18 @@
"aliases_ascii": [],
"keywords": [
"sport",
- "travel"
- ]
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball",
+ "diversity"
+ ],
+ "moji": "⛹"
},
"basketball_player_tone1": {
"unicode": "26F9-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball tone 1",
"shortname": ":basketball_player_tone1:",
"category": "activity",
@@ -1607,11 +1640,19 @@
":person_with_ball_tone1:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "sport",
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball"
+ ],
+ "moji": "⛹🏻"
},
"basketball_player_tone2": {
"unicode": "26F9-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball tone 2",
"shortname": ":basketball_player_tone2:",
"category": "activity",
@@ -1619,11 +1660,19 @@
":person_with_ball_tone2:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "sport",
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball"
+ ],
+ "moji": "⛹🏼"
},
"basketball_player_tone3": {
"unicode": "26F9-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball tone 3",
"shortname": ":basketball_player_tone3:",
"category": "activity",
@@ -1631,11 +1680,19 @@
":person_with_ball_tone3:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "sport",
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball"
+ ],
+ "moji": "⛹🏽"
},
"basketball_player_tone4": {
"unicode": "26F9-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball tone 4",
"shortname": ":basketball_player_tone4:",
"category": "activity",
@@ -1643,11 +1700,19 @@
":person_with_ball_tone4:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "sport",
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball"
+ ],
+ "moji": "⛹🏾"
},
"basketball_player_tone5": {
"unicode": "26F9-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with ball tone 5",
"shortname": ":basketball_player_tone5:",
"category": "activity",
@@ -1655,14 +1720,33 @@
":person_with_ball_tone5:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "sport",
+ "travel",
+ "men",
+ "game",
+ "ball",
+ "basketball"
+ ],
+ "moji": "⛹🏿"
+ },
+ "bat": {
+ "unicode": "1F987",
+ "unicode_alternates": [],
+ "name": "bat",
+ "shortname": ":bat:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦇"
},
"bath": {
"unicode": "1F6C0",
"unicode_alternates": [],
"name": "bath",
"shortname": ":bath:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1677,16 +1761,17 @@
"bathroom",
"soap",
"water",
- "clean",
"shampoo",
"lather",
- "water"
+ "tired",
+ "diversity",
+ "steam"
],
"moji": "🛀"
},
"bath_tone1": {
"unicode": "1F6C0-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bath tone 1",
"shortname": ":bath_tone1:",
"category": "activity",
@@ -1705,11 +1790,12 @@
"clean",
"shampoo",
"lather"
- ]
+ ],
+ "moji": "🛀🏻"
},
"bath_tone2": {
"unicode": "1F6C0-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bath tone 2",
"shortname": ":bath_tone2:",
"category": "activity",
@@ -1728,11 +1814,12 @@
"clean",
"shampoo",
"lather"
- ]
+ ],
+ "moji": "🛀🏼"
},
"bath_tone3": {
"unicode": "1F6C0-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bath tone 3",
"shortname": ":bath_tone3:",
"category": "activity",
@@ -1751,11 +1838,12 @@
"clean",
"shampoo",
"lather"
- ]
+ ],
+ "moji": "🛀🏽"
},
"bath_tone4": {
"unicode": "1F6C0-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bath tone 4",
"shortname": ":bath_tone4:",
"category": "activity",
@@ -1774,11 +1862,12 @@
"clean",
"shampoo",
"lather"
- ]
+ ],
+ "moji": "🛀🏾"
},
"bath_tone5": {
"unicode": "1F6C0-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bath tone 5",
"shortname": ":bath_tone5:",
"category": "activity",
@@ -1797,7 +1886,8 @@
"clean",
"shampoo",
"lather"
- ]
+ ],
+ "moji": "🛀🏿"
},
"bathtub": {
"unicode": "1F6C1",
@@ -1819,10 +1909,11 @@
"bathroom",
"soap",
"water",
- "clean",
"shampoo",
"lather",
- "water"
+ "object",
+ "tired",
+ "steam"
],
"moji": "🛁"
},
@@ -1837,7 +1928,8 @@
"keywords": [
"energy",
"power",
- "sustain"
+ "sustain",
+ "object"
],
"moji": "🔋"
},
@@ -1846,7 +1938,7 @@
"unicode_alternates": [],
"name": "beach with umbrella",
"shortname": ":beach:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":beach_with_umbrella:"
],
@@ -1859,12 +1951,18 @@
"relaxation",
"tanning",
"tan",
- "swimming"
- ]
+ "swimming",
+ "places",
+ "travel",
+ "tropical",
+ "beach",
+ "swim"
+ ],
+ "moji": "🏖"
},
"beach_umbrella": {
"unicode": "26F1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "umbrella on ground",
"shortname": ":beach_umbrella:",
"category": "objects",
@@ -1877,8 +1975,11 @@
"rain",
"sun",
"travel",
- "weather"
- ]
+ "weather",
+ "vacation",
+ "tropical"
+ ],
+ "moji": "⛱"
},
"bear": {
"unicode": "1F43B",
@@ -1890,7 +1991,9 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife",
+ "roar"
],
"moji": "🐻"
},
@@ -1899,7 +2002,7 @@
"unicode_alternates": [],
"name": "bed",
"shortname": ":bed:",
- "category": "travel_places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1909,8 +2012,11 @@
"full",
"twin",
"king",
- "mattress"
- ]
+ "mattress",
+ "object",
+ "tired"
+ ],
+ "moji": "🛏"
},
"bee": {
"unicode": "1F41D",
@@ -1932,7 +2038,8 @@
"honey",
"hive",
"bumble",
- "pollination"
+ "pollination",
+ "insects"
],
"moji": "🐝"
},
@@ -1941,7 +2048,7 @@
"unicode_alternates": [],
"name": "beer mug",
"shortname": ":beer:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1962,7 +2069,9 @@
"brewery",
"micro",
"pint",
- "boot"
+ "boot",
+ "alcohol",
+ "parties"
],
"moji": "🍺"
},
@@ -1971,7 +2080,7 @@
"unicode_alternates": [],
"name": "clinking beer mugs",
"shortname": ":beers:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -1987,11 +2096,14 @@
"mug",
"toast",
"celebrate",
- "pub",
"bar",
"jolly",
"hops",
- "clink"
+ "clink",
+ "alcohol",
+ "thank you",
+ "boys night",
+ "parties"
],
"moji": "🍻"
},
@@ -2013,8 +2125,9 @@
"beetle",
"cow",
"lady cow",
- "insect",
- "endearment"
+ "endearment",
+ "insects",
+ "animal"
],
"moji": "🐞"
},
@@ -2023,12 +2136,13 @@
"unicode_alternates": [],
"name": "japanese symbol for beginner",
"shortname": ":beginner:",
- "category": "places",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"badge",
- "shield"
+ "shield",
+ "symbol"
],
"moji": "🔰"
},
@@ -2037,7 +2151,7 @@
"unicode_alternates": [],
"name": "bell",
"shortname": ":bell:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2045,7 +2159,10 @@
"christmas",
"notification",
"sound",
- "xmas"
+ "xmas",
+ "object",
+ "alarm",
+ "symbol"
],
"moji": "🔔"
},
@@ -2054,7 +2171,7 @@
"unicode_alternates": [],
"name": "bellhop bell",
"shortname": ":bellhop:",
- "category": "travel_places",
+ "category": "objects",
"aliases": [
":bellhop_bell:"
],
@@ -2062,15 +2179,17 @@
"keywords": [
"hotel",
"porter",
- "ding"
- ]
+ "ding",
+ "object"
+ ],
+ "moji": "🛎"
},
"bento": {
"unicode": "1F371",
"unicode_alternates": [],
"name": "bento box",
"shortname": ":bento:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2078,13 +2197,14 @@
"food",
"japanese",
"bento",
- "japanese",
"rice",
"meal",
- "box",
"obento",
"convenient",
- "lunchbox"
+ "lunchbox",
+ "object",
+ "sushi",
+ "japan"
],
"moji": "🍱"
},
@@ -2093,7 +2213,7 @@
"unicode_alternates": [],
"name": "bicyclist",
"shortname": ":bicyclist:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2103,16 +2223,19 @@
"sports",
"bicyclist",
"road",
- "bike",
"pedal",
"bicycle",
- "transportation"
+ "transportation",
+ "men",
+ "workout",
+ "sport",
+ "diversity"
],
"moji": "🚴"
},
"bicyclist_tone1": {
"unicode": "1F6B4-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bicyclist tone 1",
"shortname": ":bicyclist_tone1:",
"category": "activity",
@@ -2127,11 +2250,12 @@
"pedal",
"bicycle",
"transportation"
- ]
+ ],
+ "moji": "🚴🏻"
},
"bicyclist_tone2": {
"unicode": "1F6B4-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bicyclist tone 2",
"shortname": ":bicyclist_tone2:",
"category": "activity",
@@ -2146,11 +2270,12 @@
"pedal",
"bicycle",
"transportation"
- ]
+ ],
+ "moji": "🚴🏼"
},
"bicyclist_tone3": {
"unicode": "1F6B4-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bicyclist tone 3",
"shortname": ":bicyclist_tone3:",
"category": "activity",
@@ -2165,11 +2290,12 @@
"pedal",
"bicycle",
"transportation"
- ]
+ ],
+ "moji": "🚴🏽"
},
"bicyclist_tone4": {
"unicode": "1F6B4-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bicyclist tone 4",
"shortname": ":bicyclist_tone4:",
"category": "activity",
@@ -2184,11 +2310,12 @@
"pedal",
"bicycle",
"transportation"
- ]
+ ],
+ "moji": "🚴🏾"
},
"bicyclist_tone5": {
"unicode": "1F6B4-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bicyclist tone 5",
"shortname": ":bicyclist_tone5:",
"category": "activity",
@@ -2203,14 +2330,15 @@
"pedal",
"bicycle",
"transportation"
- ]
+ ],
+ "moji": "🚴🏿"
},
"bike": {
"unicode": "1F6B2",
"unicode_alternates": [],
"name": "bicycle",
"shortname": ":bike:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2220,8 +2348,8 @@
"sports",
"bike",
"pedal",
- "bicycle",
- "transportation"
+ "transportation",
+ "travel"
],
"moji": "🚲"
},
@@ -2230,7 +2358,7 @@
"unicode_alternates": [],
"name": "bikini",
"shortname": ":bikini:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2239,13 +2367,18 @@
"female",
"girl",
"swimming",
- "woman"
+ "woman",
+ "women",
+ "sexy",
+ "vacation",
+ "tropical",
+ "swim"
],
"moji": "👙"
},
"biohazard": {
"unicode": "2623",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "biohazard sign",
"shortname": ":biohazard:",
"category": "symbols",
@@ -2254,8 +2387,10 @@
],
"aliases_ascii": [],
"keywords": [
- "symbol"
- ]
+ "symbol",
+ "science"
+ ],
+ "moji": "☣"
},
"bird": {
"unicode": "1F426",
@@ -2269,7 +2404,8 @@
"animal",
"fly",
"nature",
- "tweet"
+ "tweet",
+ "wildlife"
],
"moji": "🐦"
},
@@ -2278,7 +2414,7 @@
"unicode_alternates": [],
"name": "birthday cake",
"shortname": ":birthday:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2286,10 +2422,11 @@
"party",
"birthday",
"birth",
- "cake",
"dessert",
"wish",
- "celebrate"
+ "celebrate",
+ "food",
+ "parties"
],
"moji": "🎂"
},
@@ -2300,26 +2437,42 @@
],
"name": "medium black circle",
"shortname": ":black_circle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "circle"
],
"moji": "⚫"
},
+ "black_heart": {
+ "unicode": "1F5A4",
+ "unicode_alternates": [],
+ "name": "black heart",
+ "shortname": ":black_heart:",
+ "category": "symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🖤"
+ },
"black_joker": {
"unicode": "1F0CF",
"unicode_alternates": [],
"name": "playing card black joker",
"shortname": ":black_joker:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cards",
"game",
- "poker"
+ "poker",
+ "object",
+ "symbol"
],
"moji": "🃏"
},
@@ -2330,11 +2483,14 @@
],
"name": "black large square",
"shortname": ":black_large_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "⬛"
},
@@ -2345,10 +2501,14 @@
],
"name": "black medium small square",
"shortname": ":black_medium_small_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "shapes",
+ "symbol",
+ "square"
+ ],
"moji": "◾"
},
"black_medium_square": {
@@ -2358,11 +2518,14 @@
],
"name": "black medium square",
"shortname": ":black_medium_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "◼"
},
@@ -2378,7 +2541,10 @@
"aliases_ascii": [],
"keywords": [
"pen",
- "stationery"
+ "stationery",
+ "object",
+ "office",
+ "write"
],
"moji": "✒"
},
@@ -2389,10 +2555,14 @@
],
"name": "black small square",
"shortname": ":black_small_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "shapes",
+ "symbol",
+ "square"
+ ],
"moji": "▪"
},
"black_square_button": {
@@ -2400,11 +2570,14 @@
"unicode_alternates": [],
"name": "black square button",
"shortname": ":black_square_button:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "frame"
+ "frame",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "🔲"
},
@@ -2422,7 +2595,8 @@
"yellow",
"blossom",
"daisy",
- "flower"
+ "flower",
+ "plant"
],
"moji": "🌼"
},
@@ -2445,7 +2619,9 @@
"ballonfish",
"toadfish",
"fugu fish",
- "sushi"
+ "sushi",
+ "wildlife",
+ "animal"
],
"moji": "🐡"
},
@@ -2460,7 +2636,11 @@
"keywords": [
"knowledge",
"library",
- "read"
+ "read",
+ "object",
+ "office",
+ "write",
+ "book"
],
"moji": "📘"
},
@@ -2469,15 +2649,16 @@
"unicode_alternates": [],
"name": "recreational vehicle",
"shortname": ":blue_car:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"car",
"suv",
- "car",
"wagon",
- "automobile"
+ "automobile",
+ "transportation",
+ "travel"
],
"moji": "🚙"
},
@@ -2486,7 +2667,7 @@
"unicode_alternates": [],
"name": "blue heart",
"shortname": ":blue_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2496,11 +2677,11 @@
"valentines",
"blue",
"heart",
- "love",
"stability",
"truth",
"loyalty",
- "trust"
+ "trust",
+ "symbol"
],
"moji": "💙"
},
@@ -2509,7 +2690,7 @@
"unicode_alternates": [],
"name": "smiling face with smiling eyes",
"shortname": ":blush:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2521,8 +2702,10 @@
"shy",
"smile",
"smiling",
- "smile",
- "smiley"
+ "smiley",
+ "emotion",
+ "good",
+ "beautiful"
],
"moji": "😊"
},
@@ -2536,7 +2719,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐗"
},
@@ -2550,7 +2734,11 @@
"aliases_ascii": [],
"keywords": [
"boom",
- "explode"
+ "explode",
+ "object",
+ "weapon",
+ "dead",
+ "blast"
],
"moji": "💣"
},
@@ -2564,26 +2752,14 @@
"aliases_ascii": [],
"keywords": [
"library",
- "literature"
+ "literature",
+ "object",
+ "office",
+ "write",
+ "book"
],
"moji": "📖"
},
- "book2": {
- "unicode": "1F56E",
- "unicode_alternates": [],
- "name": "book",
- "shortname": ":book2:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "library",
- "literature",
- "novel",
- "reading",
- "story"
- ]
- },
"bookmark": {
"unicode": "1F516",
"unicode_alternates": [],
@@ -2593,7 +2769,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "favorite"
+ "favorite",
+ "object",
+ "book"
],
"moji": "🔖"
},
@@ -2606,7 +2784,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "favorite"
+ "favorite",
+ "office",
+ "write"
],
"moji": "📑"
},
@@ -2620,7 +2800,11 @@
"aliases_ascii": [],
"keywords": [
"library",
- "literature"
+ "literature",
+ "object",
+ "office",
+ "write",
+ "book"
],
"moji": "📚"
},
@@ -2629,7 +2813,7 @@
"unicode_alternates": [],
"name": "collision symbol",
"shortname": ":boom:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2642,7 +2826,9 @@
"fire",
"emphasis",
"wow",
- "bam"
+ "bam",
+ "symbol",
+ "blast"
],
"moji": "💥"
},
@@ -2651,12 +2837,16 @@
"unicode_alternates": [],
"name": "womans boots",
"shortname": ":boot:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fashion",
- "shoes"
+ "shoes",
+ "women",
+ "shoe",
+ "sexy",
+ "accessories"
],
"moji": "👢"
},
@@ -2670,33 +2860,20 @@
"aliases_ascii": [],
"keywords": [
"flowers",
- "nature"
+ "nature",
+ "flower",
+ "plant",
+ "rip",
+ "condolence"
],
"moji": "💐"
},
- "bouquet2": {
- "unicode": "1F395",
- "unicode_alternates": [],
- "name": "bouquet of flowers",
- "shortname": ":bouquet2:",
- "category": "celebration",
- "aliases": [
- ":bouquet_of_flowers:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "nature",
- "marriage",
- "wedding",
- "bride"
- ]
- },
"bow": {
"unicode": "1F647",
"unicode_alternates": [],
"name": "person bowing deeply",
"shortname": ":bow:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2707,13 +2884,16 @@
"bow",
"respect",
"curtsy",
- "bend"
+ "bend",
+ "people",
+ "pray",
+ "diversity"
],
"moji": "🙇"
},
"bow_and_arrow": {
"unicode": "1F3F9",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bow and arrow",
"shortname": ":bow_and_arrow:",
"category": "activity",
@@ -2721,11 +2901,15 @@
":archery:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "weapon",
+ "sport"
+ ],
+ "moji": "🏹"
},
"bow_tone1": {
"unicode": "1F647-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person bowing deeply tone 1",
"shortname": ":bow_tone1:",
"category": "people",
@@ -2739,11 +2923,12 @@
"bow",
"respect",
"bend"
- ]
+ ],
+ "moji": "🙇🏻"
},
"bow_tone2": {
"unicode": "1F647-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person bowing deeply tone 2",
"shortname": ":bow_tone2:",
"category": "people",
@@ -2757,11 +2942,12 @@
"bow",
"respect",
"bend"
- ]
+ ],
+ "moji": "🙇🏼"
},
"bow_tone3": {
"unicode": "1F647-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person bowing deeply tone 3",
"shortname": ":bow_tone3:",
"category": "people",
@@ -2775,11 +2961,12 @@
"bow",
"respect",
"bend"
- ]
+ ],
+ "moji": "🙇🏽"
},
"bow_tone4": {
"unicode": "1F647-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person bowing deeply tone 4",
"shortname": ":bow_tone4:",
"category": "people",
@@ -2793,11 +2980,12 @@
"bow",
"respect",
"bend"
- ]
+ ],
+ "moji": "🙇🏾"
},
"bow_tone5": {
"unicode": "1F647-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person bowing deeply tone 5",
"shortname": ":bow_tone5:",
"category": "people",
@@ -2811,14 +2999,15 @@
"bow",
"respect",
"bend"
- ]
+ ],
+ "moji": "🙇🏿"
},
"bowling": {
"unicode": "1F3B3",
"unicode_alternates": [],
"name": "bowling",
"shortname": ":bowling:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2831,28 +3020,46 @@
"pin",
"strike",
"spare",
- "game"
+ "game",
+ "sport",
+ "boys night"
],
"moji": "🎳"
},
+ "boxing_glove": {
+ "unicode": "1F94A",
+ "unicode_alternates": [],
+ "name": "boxing glove",
+ "shortname": ":boxing_glove:",
+ "category": "activity",
+ "aliases": [
+ ":boxing_gloves:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥊"
+ },
"boy": {
"unicode": "1F466",
"unicode_alternates": [],
"name": "boy",
"shortname": ":boy:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"guy",
"male",
- "man"
+ "man",
+ "people",
+ "baby",
+ "diversity"
],
"moji": "👦"
},
"boy_tone1": {
"unicode": "1F466-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "boy tone 1",
"shortname": ":boy_tone1:",
"category": "people",
@@ -2862,11 +3069,12 @@
"male",
"kid",
"child"
- ]
+ ],
+ "moji": "👦🏻"
},
"boy_tone2": {
"unicode": "1F466-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "boy tone 2",
"shortname": ":boy_tone2:",
"category": "people",
@@ -2876,11 +3084,12 @@
"male",
"kid",
"child"
- ]
+ ],
+ "moji": "👦🏼"
},
"boy_tone3": {
"unicode": "1F466-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "boy tone 3",
"shortname": ":boy_tone3:",
"category": "people",
@@ -2890,11 +3099,12 @@
"male",
"kid",
"child"
- ]
+ ],
+ "moji": "👦🏽"
},
"boy_tone4": {
"unicode": "1F466-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "boy tone 4",
"shortname": ":boy_tone4:",
"category": "people",
@@ -2904,11 +3114,12 @@
"male",
"kid",
"child"
- ]
+ ],
+ "moji": "👦🏾"
},
"boy_tone5": {
"unicode": "1F466-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "boy tone 5",
"shortname": ":boy_tone5:",
"category": "people",
@@ -2918,27 +3129,15 @@
"male",
"kid",
"child"
- ]
- },
- "boys_symbol": {
- "unicode": "1F6C9",
- "unicode_alternates": [],
- "name": "boys symbol",
- "shortname": ":boys_symbol:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "male",
- "child"
- ]
+ ],
+ "moji": "👦🏿"
},
"bread": {
"unicode": "1F35E",
"unicode_alternates": [],
"name": "bread",
"shortname": ":bread:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2957,7 +3156,7 @@
"unicode_alternates": [],
"name": "bride with veil",
"shortname": ":bride_with_veil:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -2965,19 +3164,21 @@
"marriage",
"wedding",
"bride",
- "wedding",
"planning",
"veil",
"gown",
"dress",
"engagement",
- "white"
+ "white",
+ "people",
+ "women",
+ "diversity"
],
"moji": "👰"
},
"bride_with_veil_tone1": {
"unicode": "1F470-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bride with veil tone 1",
"shortname": ":bride_with_veil_tone1:",
"category": "people",
@@ -2987,17 +3188,17 @@
"couple",
"marriage",
"wedding",
- "wedding",
"planning",
"gown",
"dress",
"engagement",
"white"
- ]
+ ],
+ "moji": "👰🏻"
},
"bride_with_veil_tone2": {
"unicode": "1F470-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bride with veil tone 2",
"shortname": ":bride_with_veil_tone2:",
"category": "people",
@@ -3007,17 +3208,17 @@
"couple",
"marriage",
"wedding",
- "wedding",
"planning",
"gown",
"dress",
"engagement",
"white"
- ]
+ ],
+ "moji": "👰🏼"
},
"bride_with_veil_tone3": {
"unicode": "1F470-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bride with veil tone 3",
"shortname": ":bride_with_veil_tone3:",
"category": "people",
@@ -3027,17 +3228,17 @@
"couple",
"marriage",
"wedding",
- "wedding",
"planning",
"gown",
"dress",
"engagement",
"white"
- ]
+ ],
+ "moji": "👰🏽"
},
"bride_with_veil_tone4": {
"unicode": "1F470-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bride with veil tone 4",
"shortname": ":bride_with_veil_tone4:",
"category": "people",
@@ -3047,17 +3248,17 @@
"couple",
"marriage",
"wedding",
- "wedding",
"planning",
"gown",
"dress",
"engagement",
"white"
- ]
+ ],
+ "moji": "👰🏾"
},
"bride_with_veil_tone5": {
"unicode": "1F470-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bride with veil tone 5",
"shortname": ":bride_with_veil_tone5:",
"category": "people",
@@ -3067,20 +3268,20 @@
"couple",
"marriage",
"wedding",
- "wedding",
"planning",
"gown",
"dress",
"engagement",
"white"
- ]
+ ],
+ "moji": "👰🏿"
},
"bridge_at_night": {
"unicode": "1F309",
"unicode_alternates": [],
"name": "bridge at night",
"shortname": ":bridge_at_night:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3093,7 +3294,11 @@
"evening",
"suspension",
"golden",
- "gate"
+ "gate",
+ "places",
+ "travel",
+ "vacation",
+ "goodnight"
],
"moji": "🌉"
},
@@ -3102,13 +3307,17 @@
"unicode_alternates": [],
"name": "briefcase",
"shortname": ":briefcase:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"business",
"documents",
- "work"
+ "work",
+ "bag",
+ "accessories",
+ "nutcase",
+ "job"
],
"moji": "💼"
},
@@ -3117,14 +3326,17 @@
"unicode_alternates": [],
"name": "broken heart",
"shortname": ":broken_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [
"</3"
],
"keywords": [
"sad",
- "sorry"
+ "sorry",
+ "love",
+ "symbol",
+ "heartbreak"
],
"moji": "💔"
},
@@ -3140,9 +3352,10 @@
"insect",
"nature",
"bug",
- "insect",
"virus",
- "error"
+ "error",
+ "insects",
+ "animal"
],
"moji": "🐛"
},
@@ -3159,7 +3372,8 @@
"light",
"idea",
"bulb",
- "light"
+ "object",
+ "science"
],
"moji": "💡"
},
@@ -3168,14 +3382,15 @@
"unicode_alternates": [],
"name": "high-speed train with bullet nose",
"shortname": ":bullettrain_front:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"transportation",
"train",
"bullet",
- "rail"
+ "rail",
+ "travel"
],
"moji": "🚅"
},
@@ -3184,7 +3399,7 @@
"unicode_alternates": [],
"name": "high-speed train",
"shortname": ":bullettrain_side:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3192,58 +3407,31 @@
"vehicle",
"train",
"bullet",
- "rail"
+ "rail",
+ "travel"
],
"moji": "🚄"
},
- "bullhorn": {
- "unicode": "1F56B",
- "unicode_alternates": [],
- "name": "bullhorn",
- "shortname": ":bullhorn:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "sound",
- "noise",
- "announcement",
- "megaphone"
- ]
- },
- "bullhorn_waves": {
- "unicode": "1F56C",
- "unicode_alternates": [],
- "name": "bullhorn with sound waves",
- "shortname": ":bullhorn_waves:",
- "category": "objects_symbols",
- "aliases": [
- ":bullhorn_with_sound_waves:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "sound",
- "noise",
- "announcement",
- "megaphone"
- ]
- },
"burrito": {
"unicode": "1F32F",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "burrito",
"shortname": ":burrito:",
- "category": "foods",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "food",
+ "mexican"
+ ],
+ "moji": "🌯"
},
"bus": {
"unicode": "1F68C",
"unicode_alternates": [],
"name": "bus",
"shortname": ":bus:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3253,8 +3441,8 @@
"bus",
"school",
"city",
- "transportation",
- "public"
+ "public",
+ "office"
],
"moji": "🚌"
},
@@ -3263,7 +3451,7 @@
"unicode_alternates": [],
"name": "bus stop",
"shortname": ":busstop:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3272,7 +3460,7 @@
"stop",
"city",
"transport",
- "transportation"
+ "object"
],
"moji": "🚏"
},
@@ -3281,7 +3469,7 @@
"unicode_alternates": [],
"name": "bust in silhouette",
"shortname": ":bust_in_silhouette:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3290,8 +3478,6 @@
"person",
"user",
"silhouette",
- "person",
- "user",
"member",
"account",
"guest",
@@ -3300,7 +3486,8 @@
"profile",
"me",
"myself",
- "i"
+ "i",
+ "people"
],
"moji": "👤"
},
@@ -3309,7 +3496,7 @@
"unicode_alternates": [],
"name": "busts in silhouette",
"shortname": ":busts_in_silhouette:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3322,7 +3509,6 @@
"silhouette",
"silhouettes",
"people",
- "user",
"members",
"accounts",
"relationship",
@@ -3330,6 +3516,17 @@
],
"moji": "👥"
},
+ "butterfly": {
+ "unicode": "1F98B",
+ "unicode_alternates": [],
+ "name": "butterfly",
+ "shortname": ":butterfly:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦋"
+ },
"cactus": {
"unicode": "1F335",
"unicode_alternates": [],
@@ -3346,7 +3543,8 @@
"desert",
"drought",
"spike",
- "poke"
+ "poke",
+ "trees"
],
"moji": "🌵"
},
@@ -3355,7 +3553,7 @@
"unicode_alternates": [],
"name": "shortcake",
"shortname": ":cake:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3368,24 +3566,6 @@
],
"moji": "🍰"
},
- "calculator": {
- "unicode": "1F5A9",
- "unicode_alternates": [],
- "name": "pocket calculator",
- "shortname": ":calculator:",
- "category": "objects_symbols",
- "aliases": [
- ":pocket calculator:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "add",
- "subtract",
- "multiple",
- "divide",
- "scientific"
- ]
- },
"calendar": {
"unicode": "1F4C6",
"unicode_alternates": [],
@@ -3395,7 +3575,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "schedule"
+ "schedule",
+ "object",
+ "office"
],
"moji": "📆"
},
@@ -3404,7 +3586,7 @@
"unicode_alternates": [],
"name": "spiral calendar pad",
"shortname": ":calendar_spiral:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":spiral_calendar_pad:"
],
@@ -3412,8 +3594,89 @@
"keywords": [
"schedule",
"date",
- "day"
- ]
+ "day",
+ "object",
+ "office"
+ ],
+ "moji": "🗓"
+ },
+ "call_me": {
+ "unicode": "1F919",
+ "unicode_alternates": [],
+ "name": "call me hand",
+ "shortname": ":call_me:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙"
+ },
+ "call_me_tone1": {
+ "unicode": "1F919-1F3FB",
+ "unicode_alternates": [],
+ "name": "call me hand tone 1",
+ "shortname": ":call_me_tone1:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙🏻"
+ },
+ "call_me_tone2": {
+ "unicode": "1F919-1F3FC",
+ "unicode_alternates": [],
+ "name": "call me hand tone 2",
+ "shortname": ":call_me_tone2:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙🏼"
+ },
+ "call_me_tone3": {
+ "unicode": "1F919-1F3FD",
+ "unicode_alternates": [],
+ "name": "call me hand tone 3",
+ "shortname": ":call_me_tone3:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙🏽"
+ },
+ "call_me_tone4": {
+ "unicode": "1F919-1F3FE",
+ "unicode_alternates": [],
+ "name": "call me hand tone 4",
+ "shortname": ":call_me_tone4:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙🏾"
+ },
+ "call_me_tone5": {
+ "unicode": "1F919-1F3FF",
+ "unicode_alternates": [],
+ "name": "call me hand tone 5",
+ "shortname": ":call_me_tone5:",
+ "category": "people",
+ "aliases": [
+ ":call_me_hand_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤙🏿"
},
"calling": {
"unicode": "1F4F2",
@@ -3425,7 +3688,10 @@
"aliases_ascii": [],
"keywords": [
"incoming",
- "iphone"
+ "iphone",
+ "electronics",
+ "phone",
+ "selfie"
],
"moji": "📲"
},
@@ -3447,11 +3713,11 @@
"desert",
"central asia",
"heat",
- "hot",
"water",
"hump day",
"wednesday",
- "sex"
+ "sex",
+ "wildlife"
],
"moji": "🐫"
},
@@ -3465,7 +3731,10 @@
"aliases_ascii": [],
"keywords": [
"gadgets",
- "photo"
+ "photo",
+ "electronics",
+ "camera",
+ "selfie"
],
"moji": "📷"
},
@@ -3474,20 +3743,23 @@
"unicode_alternates": [],
"name": "camera with flash",
"shortname": ":camera_with_flash:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"photo",
- "picture"
- ]
+ "picture",
+ "electronics",
+ "camera"
+ ],
+ "moji": "📸"
},
"camping": {
"unicode": "1F3D5",
"unicode_alternates": [],
"name": "camping",
"shortname": ":camping:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3495,22 +3767,13 @@
"nature",
"wilderness",
"roughing",
- "activity"
- ]
- },
- "cancellation_x": {
- "unicode": "1F5D9",
- "unicode_alternates": [],
- "name": "cancellation x",
- "shortname": ":cancellation_x:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "cancel",
- "stop",
- "delete"
- ]
+ "activity",
+ "places",
+ "travel",
+ "vacation",
+ "camp"
+ ],
+ "moji": "🏕"
},
"cancer": {
"unicode": "264B",
@@ -3519,7 +3782,7 @@
],
"name": "cancer",
"shortname": ":cancer:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3531,9 +3794,8 @@
"stars",
"zodiac",
"sign",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♋"
},
@@ -3542,20 +3804,22 @@
"unicode_alternates": [],
"name": "candle",
"shortname": ":candle:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"light",
- "wax"
- ]
+ "wax",
+ "object"
+ ],
+ "moji": "🕯"
},
"candy": {
"unicode": "1F36C",
"unicode_alternates": [],
"name": "candy",
"shortname": ":candy:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3564,22 +3828,38 @@
"candy",
"sugar",
"sweet",
- "hard"
+ "hard",
+ "food",
+ "halloween"
],
"moji": "🍬"
},
+ "canoe": {
+ "unicode": "1F6F6",
+ "unicode_alternates": [],
+ "name": "canoe",
+ "shortname": ":canoe:",
+ "category": "travel",
+ "aliases": [
+ ":kayak:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🛶"
+ },
"capital_abcd": {
"unicode": "1F520",
"unicode_alternates": [],
"name": "input symbol for latin capital letters",
"shortname": ":capital_abcd:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"blue-square",
- "words"
+ "words",
+ "symbol"
],
"moji": "🔠"
},
@@ -3590,7 +3870,7 @@
],
"name": "capricorn",
"shortname": ":capricorn:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3603,9 +3883,8 @@
"stars",
"zodiac",
"sign",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♑"
},
@@ -3614,15 +3893,19 @@
"unicode_alternates": [],
"name": "card file box",
"shortname": ":card_box:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":card_file_box:"
],
"aliases_ascii": [],
"keywords": [
"index",
- "organization"
- ]
+ "organization",
+ "object",
+ "work",
+ "office"
+ ],
+ "moji": "🗃"
},
"card_index": {
"unicode": "1F4C7",
@@ -3634,7 +3917,10 @@
"aliases_ascii": [],
"keywords": [
"business",
- "stationery"
+ "stationery",
+ "object",
+ "work",
+ "office"
],
"moji": "📇"
},
@@ -3643,7 +3929,7 @@
"unicode_alternates": [],
"name": "carousel horse",
"shortname": ":carousel_horse:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3651,37 +3937,106 @@
"horse",
"photo",
"carousel",
- "horse",
"amusement",
"park",
"ride",
"entertainment",
- "park",
- "fair"
+ "fair",
+ "places",
+ "object",
+ "vacation",
+ "roller coaster"
],
"moji": "🎠"
},
- "cartridge": {
- "unicode": "1F5AD",
+ "carrot": {
+ "unicode": "1F955",
"unicode_alternates": [],
- "name": "tape cartridge",
- "shortname": ":cartridge:",
- "category": "objects_symbols",
+ "name": "carrot",
+ "shortname": ":carrot:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥕"
+ },
+ "cartwheel": {
+ "unicode": "1F938",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel",
+ "shortname": ":cartwheel:",
+ "category": "activity",
"aliases": [
- ":tape_cartridge:"
+ ":person_doing_cartwheel:"
],
"aliases_ascii": [],
- "keywords": [
- "oldschool",
- "save",
- "technology",
- "disk",
- "storage",
- "information",
- "computer",
- "drive",
- "megabyte"
- ]
+ "keywords": [],
+ "moji": "🤸"
+ },
+ "cartwheel_tone1": {
+ "unicode": "1F938-1F3FB",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel tone 1",
+ "shortname": ":cartwheel_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":person_doing_cartwheel_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤸🏻"
+ },
+ "cartwheel_tone2": {
+ "unicode": "1F938-1F3FC",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel tone 2",
+ "shortname": ":cartwheel_tone2:",
+ "category": "activity",
+ "aliases": [
+ ":person_doing_cartwheel_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤸🏼"
+ },
+ "cartwheel_tone3": {
+ "unicode": "1F938-1F3FD",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel tone 3",
+ "shortname": ":cartwheel_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":person_doing_cartwheel_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤸🏽"
+ },
+ "cartwheel_tone4": {
+ "unicode": "1F938-1F3FE",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel tone 4",
+ "shortname": ":cartwheel_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":person_doing_cartwheel_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤸🏾,"
+ },
+ "cartwheel_tone5": {
+ "unicode": "1F938-1F3FF",
+ "unicode_alternates": [],
+ "name": "person doing cartwheel tone 5",
+ "shortname": ":cartwheel_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":person_doing_cartwheel_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤸🏿"
},
"cat": {
"unicode": "1F431",
@@ -3693,7 +4048,10 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "meow"
+ "meow",
+ "halloween",
+ "vagina",
+ "cat"
],
"moji": "🐱"
},
@@ -3711,13 +4069,13 @@
"pet",
"cat",
"kitten",
- "meow"
+ "halloween"
],
"moji": "🐈"
},
"cd": {
"unicode": "1F4BF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "optical disc",
"shortname": ":cd:",
"category": "objects",
@@ -3732,25 +4090,14 @@
"cd",
"computer",
"object",
- "office"
- ]
- },
- "celtic_cross": {
- "unicode": "1F548",
- "unicode_alternates": [],
- "name": "celtic cross",
- "shortname": ":celtic_cross:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "religion",
- "symbol"
- ]
+ "office",
+ "electronics"
+ ],
+ "moji": "💿"
},
"chains": {
"unicode": "26D3",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "chains",
"shortname": ":chains:",
"category": "objects",
@@ -3758,32 +4105,55 @@
"aliases_ascii": [],
"keywords": [
"chain",
- "object"
- ]
+ "object",
+ "tool"
+ ],
+ "moji": "⛓"
},
"champagne": {
"unicode": "1F37E",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bottle with popping cork",
"shortname": ":champagne:",
- "category": "foods",
+ "category": "food",
"aliases": [
":bottle_with_popping_cork:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "drink",
+ "cheers",
+ "alcohol",
+ "parties"
+ ],
+ "moji": "🍾"
+ },
+ "champagne_glass": {
+ "unicode": "1F942",
+ "unicode_alternates": [],
+ "name": "clinking glasses",
+ "shortname": ":champagne_glass:",
+ "category": "food",
+ "aliases": [
+ ":clinking_glass:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥂"
},
"chart": {
"unicode": "1F4B9",
"unicode_alternates": [],
"name": "chart with upwards trend and yen sign",
"shortname": ":chart:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"graph",
- "green-square"
+ "green-square",
+ "symbol",
+ "money"
],
"moji": "💹"
},
@@ -3796,7 +4166,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "graph"
+ "graph",
+ "work",
+ "office"
],
"moji": "📉"
},
@@ -3809,7 +4181,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "graph"
+ "graph",
+ "work",
+ "office"
],
"moji": "📈"
},
@@ -3818,7 +4192,7 @@
"unicode_alternates": [],
"name": "chequered flag",
"shortname": ":checkered_flag:",
- "category": "objects",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3832,28 +4206,32 @@
"flag",
"finish",
"complete",
- "end"
+ "end",
+ "object"
],
"moji": "🏁"
},
"cheese": {
"unicode": "1F9C0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "cheese wedge",
"shortname": ":cheese:",
- "category": "foods",
+ "category": "food",
"aliases": [
":cheese_wedge:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "food"
+ ],
+ "moji": "🧀"
},
"cherries": {
"unicode": "1F352",
"unicode_alternates": [],
"name": "cherries",
"shortname": ":cherries:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3862,7 +4240,6 @@
"cherry",
"cherries",
"tree",
- "fruit",
"pit"
],
"moji": "🍒"
@@ -3882,7 +4259,7 @@
"cherry",
"blossom",
"tree",
- "flower"
+ "tropical"
],
"moji": "🌸"
},
@@ -3899,8 +4276,9 @@
"squirrel",
"chestnut",
"roasted",
- "food",
- "tree"
+ "tree",
+ "nature",
+ "plant"
],
"moji": "🌰"
},
@@ -3927,7 +4305,7 @@
"unicode_alternates": [],
"name": "children crossing",
"shortname": ":children_crossing:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3938,7 +4316,8 @@
"crossing",
"street",
"crosswalk",
- "slow"
+ "slow",
+ "symbol"
],
"moji": "🚸"
},
@@ -3952,15 +4331,17 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
- ]
+ "nature",
+ "wildlife"
+ ],
+ "moji": "🐿"
},
"chocolate_bar": {
"unicode": "1F36B",
"unicode_alternates": [],
"name": "chocolate bar",
"shortname": ":chocolate_bar:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3971,7 +4352,8 @@
"bar",
"candy",
"coca",
- "hershey&#039;s"
+ "hershey&#039;s",
+ "halloween"
],
"moji": "🍫"
},
@@ -3980,7 +4362,7 @@
"unicode_alternates": [],
"name": "christmas tree",
"shortname": ":christmas_tree:",
- "category": "objects",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -3990,17 +4372,17 @@
"vacation",
"xmas",
"christmas",
- "xmas",
"santa",
"holiday",
"winter",
- "december",
- "santa",
"evergreen",
"ornaments",
"jesus",
"gifts",
- "presents"
+ "presents",
+ "plant",
+ "holidays",
+ "trees"
],
"moji": "🎄"
},
@@ -4011,13 +4393,16 @@
],
"name": "church",
"shortname": ":church:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"building",
"christ",
- "religion"
+ "religion",
+ "places",
+ "wedding",
+ "condolence"
],
"moji": "⛪"
},
@@ -4026,7 +4411,7 @@
"unicode_alternates": [],
"name": "cinema",
"shortname": ":cinema:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4035,10 +4420,11 @@
"movie",
"record",
"cinema",
- "movie",
"theater",
"motion",
- "picture"
+ "picture",
+ "symbol",
+ "camera"
],
"moji": "🎦"
},
@@ -4047,7 +4433,7 @@
"unicode_alternates": [],
"name": "circus tent",
"shortname": ":circus_tent:",
- "category": "places",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4057,10 +4443,10 @@
"circus",
"tent",
"event",
- "carnival",
"big",
"top",
- "canvas"
+ "canvas",
+ "circus tent"
],
"moji": "🎪"
},
@@ -4069,7 +4455,7 @@
"unicode_alternates": [],
"name": "cityscape at dusk",
"shortname": ":city_dusk:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4082,7 +4468,9 @@
"evening",
"metropolitan",
"night",
- "dark"
+ "dark",
+ "places",
+ "building"
],
"moji": "🌆"
},
@@ -4091,7 +4479,7 @@
"unicode_alternates": [],
"name": "sunset over buildings",
"shortname": ":city_sunset:",
- "category": "places",
+ "category": "travel",
"aliases": [
":city_sunrise:"
],
@@ -4106,7 +4494,11 @@
"morning",
"metropolitan",
"rise",
- "sun"
+ "sun",
+ "places",
+ "building",
+ "sky",
+ "vacation"
],
"moji": "🌇"
},
@@ -4115,7 +4507,7 @@
"unicode_alternates": [],
"name": "cityscape",
"shortname": ":cityscape:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4124,12 +4516,16 @@
"view",
"lights",
"buiildings",
- "metropolis"
- ]
+ "metropolis",
+ "places",
+ "building",
+ "vacation"
+ ],
+ "moji": "🏙"
},
"cl": {
"unicode": "1F191",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "squared cl",
"shortname": ":cl:",
"category": "symbols",
@@ -4143,14 +4539,15 @@
"clear",
"symbol",
"word"
- ]
+ ],
+ "moji": "🆑"
},
"clap": {
"unicode": "1F44F",
"unicode_alternates": [],
"name": "clapping hands sign",
"shortname": ":clap:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4163,13 +4560,18 @@
"approval",
"sound",
"encouragement",
- "enthusiasm"
+ "enthusiasm",
+ "body",
+ "win",
+ "diversity",
+ "good",
+ "beautiful"
],
"moji": "👏"
},
"clap_tone1": {
"unicode": "1F44F-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clapping hands sign tone 1",
"shortname": ":clap_tone1:",
"category": "people",
@@ -4185,11 +4587,12 @@
"sound",
"encouragement",
"enthusiasm"
- ]
+ ],
+ "moji": "👏🏻"
},
"clap_tone2": {
"unicode": "1F44F-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clapping hands sign tone 2",
"shortname": ":clap_tone2:",
"category": "people",
@@ -4205,11 +4608,12 @@
"sound",
"encouragement",
"enthusiasm"
- ]
+ ],
+ "moji": "👏🏼"
},
"clap_tone3": {
"unicode": "1F44F-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clapping hands sign tone 3",
"shortname": ":clap_tone3:",
"category": "people",
@@ -4225,11 +4629,12 @@
"sound",
"encouragement",
"enthusiasm"
- ]
+ ],
+ "moji": "👏🏽"
},
"clap_tone4": {
"unicode": "1F44F-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clapping hands sign tone 4",
"shortname": ":clap_tone4:",
"category": "people",
@@ -4245,11 +4650,12 @@
"sound",
"encouragement",
"enthusiasm"
- ]
+ ],
+ "moji": "👏🏾"
},
"clap_tone5": {
"unicode": "1F44F-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clapping hands sign tone 5",
"shortname": ":clap_tone5:",
"category": "people",
@@ -4265,14 +4671,15 @@
"sound",
"encouragement",
"enthusiasm"
- ]
+ ],
+ "moji": "👏🏿"
},
"clapper": {
"unicode": "1F3AC",
"unicode_alternates": [],
"name": "clapper board",
"shortname": ":clapper:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4282,8 +4689,6 @@
"clapper",
"board",
"clapboard",
- "movie",
- "film",
"take"
],
"moji": "🎬"
@@ -4293,7 +4698,7 @@
"unicode_alternates": [],
"name": "classical building",
"shortname": ":classical_building:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4301,8 +4706,13 @@
"architecture",
"history",
"iconic",
- "genre"
- ]
+ "genre",
+ "places",
+ "building",
+ "travel",
+ "vacation"
+ ],
+ "moji": "🏛"
},
"clipboard": {
"unicode": "1F4CB",
@@ -4314,7 +4724,11 @@
"aliases_ascii": [],
"keywords": [
"documents",
- "stationery"
+ "stationery",
+ "object",
+ "work",
+ "office",
+ "write"
],
"moji": "📋"
},
@@ -4323,26 +4737,29 @@
"unicode_alternates": [],
"name": "mantlepiece clock",
"shortname": ":clock:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":mantlepiece_clock:"
],
"aliases_ascii": [],
"keywords": [
- "time"
- ]
+ "time",
+ "object"
+ ],
+ "moji": "🕰"
},
"clock1": {
"unicode": "1F550",
"unicode_alternates": [],
"name": "clock face one oclock",
"shortname": ":clock1:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕐"
},
@@ -4351,12 +4768,13 @@
"unicode_alternates": [],
"name": "clock face ten oclock",
"shortname": ":clock10:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕙"
},
@@ -4365,12 +4783,13 @@
"unicode_alternates": [],
"name": "clock face ten-thirty",
"shortname": ":clock1030:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕥"
},
@@ -4379,12 +4798,13 @@
"unicode_alternates": [],
"name": "clock face eleven oclock",
"shortname": ":clock11:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕚"
},
@@ -4393,12 +4813,13 @@
"unicode_alternates": [],
"name": "clock face eleven-thirty",
"shortname": ":clock1130:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕦"
},
@@ -4407,12 +4828,13 @@
"unicode_alternates": [],
"name": "clock face twelve oclock",
"shortname": ":clock12:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕛"
},
@@ -4421,25 +4843,28 @@
"unicode_alternates": [],
"name": "clock face twelve-thirty",
"shortname": ":clock1230:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
- ]
+ "time",
+ "symbol"
+ ],
+ "moji": "🕧"
},
"clock130": {
"unicode": "1F55C",
"unicode_alternates": [],
"name": "clock face one-thirty",
"shortname": ":clock130:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕜"
},
@@ -4448,12 +4873,13 @@
"unicode_alternates": [],
"name": "clock face two oclock",
"shortname": ":clock2:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕑"
},
@@ -4462,12 +4888,13 @@
"unicode_alternates": [],
"name": "clock face two-thirty",
"shortname": ":clock230:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕝"
},
@@ -4476,12 +4903,13 @@
"unicode_alternates": [],
"name": "clock face three oclock",
"shortname": ":clock3:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕒"
},
@@ -4490,12 +4918,13 @@
"unicode_alternates": [],
"name": "clock face three-thirty",
"shortname": ":clock330:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕞"
},
@@ -4504,12 +4933,13 @@
"unicode_alternates": [],
"name": "clock face four oclock",
"shortname": ":clock4:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕓"
},
@@ -4518,12 +4948,13 @@
"unicode_alternates": [],
"name": "clock face four-thirty",
"shortname": ":clock430:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕟"
},
@@ -4532,12 +4963,13 @@
"unicode_alternates": [],
"name": "clock face five oclock",
"shortname": ":clock5:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕔"
},
@@ -4546,12 +4978,13 @@
"unicode_alternates": [],
"name": "clock face five-thirty",
"shortname": ":clock530:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕠"
},
@@ -4560,12 +4993,13 @@
"unicode_alternates": [],
"name": "clock face six oclock",
"shortname": ":clock6:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕕"
},
@@ -4574,12 +5008,13 @@
"unicode_alternates": [],
"name": "clock face six-thirty",
"shortname": ":clock630:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕡"
},
@@ -4588,12 +5023,13 @@
"unicode_alternates": [],
"name": "clock face seven oclock",
"shortname": ":clock7:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕖"
},
@@ -4602,12 +5038,13 @@
"unicode_alternates": [],
"name": "clock face seven-thirty",
"shortname": ":clock730:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕢"
},
@@ -4616,12 +5053,13 @@
"unicode_alternates": [],
"name": "clock face eight oclock",
"shortname": ":clock8:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕗"
},
@@ -4630,12 +5068,13 @@
"unicode_alternates": [],
"name": "clock face eight-thirty",
"shortname": ":clock830:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕣"
},
@@ -4644,12 +5083,13 @@
"unicode_alternates": [],
"name": "clock face nine oclock",
"shortname": ":clock9:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕘"
},
@@ -4658,29 +5098,16 @@
"unicode_alternates": [],
"name": "clock face nine-thirty",
"shortname": ":clock930:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clock",
- "time"
+ "time",
+ "symbol"
],
"moji": "🕤"
},
- "clockwise_arrows": {
- "unicode": "1F5D8",
- "unicode_alternates": [],
- "name": "clockwise right and left semicircle arrows",
- "shortname": ":clockwise_arrows:",
- "category": "objects_symbols",
- "aliases": [
- ":clockwise_right_and_left_semicircle_arrows:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "sync"
- ]
- },
"closed_book": {
"unicode": "1F4D5",
"unicode_alternates": [],
@@ -4692,7 +5119,11 @@
"keywords": [
"knowledge",
"library",
- "read"
+ "read",
+ "object",
+ "office",
+ "write",
+ "book"
],
"moji": "📕"
},
@@ -4706,7 +5137,9 @@
"aliases_ascii": [],
"keywords": [
"privacy",
- "security"
+ "security",
+ "object",
+ "lock"
],
"moji": "🔐"
},
@@ -4715,7 +5148,7 @@
"unicode_alternates": [],
"name": "closed umbrella",
"shortname": ":closed_umbrella:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4724,12 +5157,14 @@
"weather",
"umbrella",
"closed",
- "rain",
"moisture",
"protection",
"sun",
"ultraviolet",
- "uv"
+ "uv",
+ "object",
+ "sky",
+ "accessories"
],
"moji": "🌂"
},
@@ -4745,7 +5180,10 @@
"aliases_ascii": [],
"keywords": [
"sky",
- "weather"
+ "weather",
+ "cloud",
+ "cold",
+ "rain"
],
"moji": "☁"
},
@@ -4761,8 +5199,13 @@
"aliases_ascii": [],
"keywords": [
"weather",
- "thunder"
- ]
+ "thunder",
+ "sky",
+ "cloud",
+ "cold",
+ "rain"
+ ],
+ "moji": "🌩"
},
"cloud_rain": {
"unicode": "1F327",
@@ -4776,8 +5219,14 @@
"aliases_ascii": [],
"keywords": [
"weather",
- "wet"
- ]
+ "wet",
+ "winter",
+ "sky",
+ "cloud",
+ "cold",
+ "rain"
+ ],
+ "moji": "🌧"
},
"cloud_snow": {
"unicode": "1F328",
@@ -4791,8 +5240,13 @@
"aliases_ascii": [],
"keywords": [
"weather",
- "cold"
- ]
+ "cold",
+ "winter",
+ "sky",
+ "cloud",
+ "snow"
+ ],
+ "moji": "🌨"
},
"cloud_tornado": {
"unicode": "1F32A",
@@ -4807,8 +5261,24 @@
"keywords": [
"weather",
"destruction",
- "funnel"
- ]
+ "funnel",
+ "sky",
+ "cold"
+ ],
+ "moji": "🌪"
+ },
+ "clown": {
+ "unicode": "1F921",
+ "unicode_alternates": [],
+ "name": "clown face",
+ "shortname": ":clown:",
+ "category": "people",
+ "aliases": [
+ ":clown_face:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤡"
},
"clubs": {
"unicode": "2663",
@@ -4817,12 +5287,14 @@
],
"name": "black club suit",
"shortname": ":clubs:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cards",
- "poker"
+ "poker",
+ "symbol",
+ "game"
],
"moji": "♣"
},
@@ -4831,7 +5303,7 @@
"unicode_alternates": [],
"name": "cocktail glass",
"shortname": ":cocktail:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4841,11 +5313,11 @@
"drunk",
"cocktail",
"mixed",
- "drink",
- "alcohol",
"glass",
"martini",
- "bar"
+ "bar",
+ "girls night",
+ "parties"
],
"moji": "🍸"
},
@@ -4856,20 +5328,23 @@
],
"name": "hot beverage",
"shortname": ":coffee:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"beverage",
"cafe",
"drink",
- "espresso"
+ "espresso",
+ "caffeine",
+ "steam",
+ "morning"
],
"moji": "☕"
},
"coffin": {
"unicode": "26B0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "coffin",
"shortname": ":coffin:",
"category": "objects",
@@ -4877,15 +5352,18 @@
"aliases_ascii": [],
"keywords": [
"death",
- "object"
- ]
+ "object",
+ "dead",
+ "rip"
+ ],
+ "moji": "⚰"
},
"cold_sweat": {
"unicode": "1F630",
"unicode_alternates": [],
"name": "face with open mouth and cold sweat",
"shortname": ":cold_sweat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4893,13 +5371,15 @@
"nervous",
"sweat",
"exasperated",
- "frustrated"
+ "frustrated",
+ "smiley",
+ "emotion"
],
"moji": "😰"
},
"comet": {
"unicode": "2604",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "comet",
"shortname": ":comet:",
"category": "nature",
@@ -4907,20 +5387,23 @@
"aliases_ascii": [],
"keywords": [
"object",
- "space"
- ]
+ "space",
+ "sky"
+ ],
+ "moji": "☄"
},
"compression": {
"unicode": "1F5DC",
"unicode_alternates": [],
"name": "compression",
"shortname": ":compression:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"reduce"
- ]
+ ],
+ "moji": "🗜"
},
"computer": {
"unicode": "1F4BB",
@@ -4932,25 +5415,13 @@
"aliases_ascii": [],
"keywords": [
"laptop",
- "tech"
+ "tech",
+ "electronics",
+ "work",
+ "office"
],
"moji": "💻"
},
- "computer_old": {
- "unicode": "1F5B3",
- "unicode_alternates": [],
- "name": "old personal computer",
- "shortname": ":computer_old:",
- "category": "objects_symbols",
- "aliases": [
- ":old_personal_computer:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "cpu",
- "terminal"
- ]
- },
"confetti_ball": {
"unicode": "1F38A",
"unicode_alternates": [],
@@ -4962,7 +5433,6 @@
"keywords": [
"festival",
"party",
- "party",
"congratulations",
"confetti",
"ball",
@@ -4970,7 +5440,13 @@
"win",
"birthday",
"new years",
- "wedding"
+ "wedding",
+ "object",
+ "holidays",
+ "cheers",
+ "girls night",
+ "boys night",
+ "parties"
],
"moji": "🎊"
},
@@ -4979,7 +5455,7 @@
"unicode_alternates": [],
"name": "confounded face",
"shortname": ":confounded:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -4991,7 +5467,11 @@
"amaze",
"perplex",
"puzzle",
- "mystify"
+ "mystify",
+ "sad",
+ "smiley",
+ "angry",
+ "emotion"
],
"moji": "😖"
},
@@ -5000,7 +5480,7 @@
"unicode_alternates": [],
"name": "confused face",
"shortname": ":confused:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
">:\\",
@@ -5024,7 +5504,10 @@
"skeptical",
"undecided",
"uneasy",
- "hesitant"
+ "hesitant",
+ "smiley",
+ "surprised",
+ "emotion"
],
"moji": "😕"
},
@@ -5035,13 +5518,15 @@
],
"name": "circled ideograph congratulation",
"shortname": ":congratulations:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"japanese",
- "kanji"
+ "kanji",
+ "japan",
+ "symbol"
],
"moji": "㊗"
},
@@ -5050,19 +5535,20 @@
"unicode_alternates": [],
"name": "construction sign",
"shortname": ":construction:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"caution",
"progress",
- "wip"
+ "wip",
+ "object"
],
"moji": "🚧"
},
"construction_site": {
"unicode": "1F3D7",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "building construction",
"shortname": ":construction_site:",
"category": "travel",
@@ -5073,28 +5559,36 @@
"keywords": [
"site",
"work",
- "place"
- ]
+ "place",
+ "building",
+ "crane"
+ ],
+ "moji": "🏗"
},
"construction_worker": {
"unicode": "1F477",
"unicode_alternates": [],
"name": "construction worker",
"shortname": ":construction_worker:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"human",
"male",
"man",
- "wip"
+ "wip",
+ "people",
+ "hat",
+ "men",
+ "diversity",
+ "job"
],
"moji": "👷"
},
"construction_worker_tone1": {
"unicode": "1F477-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "construction worker tone 1",
"shortname": ":construction_worker_tone1:",
"category": "people",
@@ -5105,11 +5599,12 @@
"male",
"man",
"wip"
- ]
+ ],
+ "moji": "👷🏻"
},
"construction_worker_tone2": {
"unicode": "1F477-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "construction worker tone 2",
"shortname": ":construction_worker_tone2:",
"category": "people",
@@ -5120,11 +5615,12 @@
"male",
"man",
"wip"
- ]
+ ],
+ "moji": "👷🏼"
},
"construction_worker_tone3": {
"unicode": "1F477-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "construction worker tone 3",
"shortname": ":construction_worker_tone3:",
"category": "people",
@@ -5135,11 +5631,12 @@
"male",
"man",
"wip"
- ]
+ ],
+ "moji": "👷🏽"
},
"construction_worker_tone4": {
"unicode": "1F477-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "construction worker tone 4",
"shortname": ":construction_worker_tone4:",
"category": "people",
@@ -5150,11 +5647,12 @@
"male",
"man",
"wip"
- ]
+ ],
+ "moji": "👷🏾"
},
"construction_worker_tone5": {
"unicode": "1F477-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "construction worker tone 5",
"shortname": ":construction_worker_tone5:",
"category": "people",
@@ -5165,45 +5663,34 @@
"male",
"man",
"wip"
- ]
+ ],
+ "moji": "👷🏿"
},
"control_knobs": {
"unicode": "1F39B",
"unicode_alternates": [],
"name": "control knobs",
"shortname": ":control_knobs:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "dial"
- ]
- },
- "contruction_site": {
- "unicode": "1F3D7",
- "unicode_alternates": [],
- "name": "building construction",
- "shortname": ":contruction_site:",
- "category": "travel_places",
- "aliases": [
- ":building_construction:"
+ "dial",
+ "time"
],
- "aliases_ascii": [],
- "keywords": [
- "site",
- "work"
- ]
+ "moji": "🎛"
},
"convenience_store": {
"unicode": "1F3EA",
"unicode_alternates": [],
"name": "convenience store",
"shortname": ":convenience_store:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "building"
+ "building",
+ "places"
],
"moji": "🏪"
},
@@ -5212,7 +5699,7 @@
"unicode_alternates": [],
"name": "cookie",
"shortname": ":cookie:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5224,21 +5711,44 @@
"dessert",
"biscuit",
"sweet",
- "chocolate"
+ "vagina"
],
"moji": "🍪"
},
+ "cooking": {
+ "unicode": "1F373",
+ "unicode_alternates": [],
+ "name": "cooking",
+ "shortname": ":cooking:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [
+ "breakfast",
+ "food",
+ "egg",
+ "fry",
+ "pan",
+ "flat",
+ "cook",
+ "frying",
+ "cooking",
+ "utensil"
+ ],
+ "moji": "🍳"
+ },
"cool": {
"unicode": "1F192",
"unicode_alternates": [],
"name": "squared cool",
"shortname": ":cool:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "words"
+ "words",
+ "symbol"
],
"moji": "🆒"
},
@@ -5247,7 +5757,7 @@
"unicode_alternates": [],
"name": "police officer",
"shortname": ":cop:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5255,13 +5765,19 @@
"enforcement",
"law",
"man",
- "police"
+ "police",
+ "people",
+ "hat",
+ "men",
+ "diversity",
+ "job",
+ "911"
],
"moji": "👮"
},
"cop_tone1": {
"unicode": "1F46E-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "police officer tone 1",
"shortname": ":cop_tone1:",
"category": "people",
@@ -5273,11 +5789,12 @@
"law",
"man",
"cop"
- ]
+ ],
+ "moji": "👮🏻"
},
"cop_tone2": {
"unicode": "1F46E-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "police officer tone 2",
"shortname": ":cop_tone2:",
"category": "people",
@@ -5289,11 +5806,12 @@
"law",
"man",
"cop"
- ]
+ ],
+ "moji": "👮🏼"
},
"cop_tone3": {
"unicode": "1F46E-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "police officer tone 3",
"shortname": ":cop_tone3:",
"category": "people",
@@ -5305,11 +5823,12 @@
"law",
"man",
"cop"
- ]
+ ],
+ "moji": "👮🏽"
},
"cop_tone4": {
"unicode": "1F46E-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "police officer tone 4",
"shortname": ":cop_tone4:",
"category": "people",
@@ -5321,11 +5840,12 @@
"law",
"man",
"cop"
- ]
+ ],
+ "moji": "👮🏾"
},
"cop_tone5": {
"unicode": "1F46E-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "police officer tone 5",
"shortname": ":cop_tone5:",
"category": "people",
@@ -5337,7 +5857,8 @@
"law",
"man",
"cop"
- ]
+ ],
+ "moji": "👮🏿"
},
"copyright": {
"moji": "©",
@@ -5345,12 +5866,13 @@
"unicode_alternates": [],
"name": "copyright sign",
"shortname": ":copyright:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"ip",
- "license"
+ "license",
+ "symbol"
]
},
"corn": {
@@ -5358,7 +5880,7 @@
"unicode_alternates": [],
"name": "ear of maize",
"shortname": ":corn:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5367,7 +5889,6 @@
"vegetable",
"corn",
"maize",
- "food",
"iowa",
"kernel",
"popcorn",
@@ -5375,7 +5896,8 @@
"yellow",
"stalk",
"cob",
- "ear"
+ "ear",
+ "vegetables"
],
"moji": "🌽"
},
@@ -5384,7 +5906,7 @@
"unicode_alternates": [],
"name": "couch and lamp",
"shortname": ":couch:",
- "category": "travel_places",
+ "category": "objects",
"aliases": [
":couch_and_lamp:"
],
@@ -5397,15 +5919,17 @@
"leather",
"microfiber",
"sit",
- "relax"
- ]
+ "relax",
+ "object"
+ ],
+ "moji": "🛋"
},
"couple": {
"unicode": "1F46B",
"unicode_alternates": [],
"name": "man and woman holding hands",
"shortname": ":couple:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5417,7 +5941,9 @@
"love",
"marriage",
"people",
- "valentines"
+ "valentines",
+ "sex",
+ "creationism"
],
"moji": "👫"
},
@@ -5440,15 +5966,21 @@
"like",
"love",
"marriage",
- "valentines"
- ]
+ "valentines",
+ "people",
+ "gay",
+ "men",
+ "sex",
+ "lgbt"
+ ],
+ "moji": "👨‍❤️‍👨"
},
"couple_with_heart": {
"unicode": "1F491",
"unicode_alternates": [],
"name": "couple with heart",
"shortname": ":couple_with_heart:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5458,7 +5990,9 @@
"like",
"love",
"marriage",
- "valentines"
+ "valentines",
+ "people",
+ "sex"
],
"moji": "💑"
},
@@ -5481,15 +6015,20 @@
"like",
"love",
"marriage",
- "valentines"
- ]
+ "valentines",
+ "people",
+ "women",
+ "sex",
+ "lgbt"
+ ],
+ "moji": "👩‍❤️‍👩"
},
"couplekiss": {
"unicode": "1F48F",
"unicode_alternates": [],
"name": "kiss",
"shortname": ":couplekiss:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5497,7 +6036,9 @@
"like",
"love",
"marriage",
- "valentines"
+ "valentines",
+ "people",
+ "sex"
],
"moji": "💏"
},
@@ -5532,28 +6073,44 @@
"cow",
"milk",
"dairy",
- "beef",
"bessie",
"moo"
],
"moji": "🐄"
},
+ "cowboy": {
+ "unicode": "1F920",
+ "unicode_alternates": [],
+ "name": "face with cowboy hat",
+ "shortname": ":cowboy:",
+ "category": "people",
+ "aliases": [
+ ":face_with_cowboy_hat:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤠"
+ },
"crab": {
"unicode": "1F980",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "crab",
"shortname": ":crab:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "tropical",
+ "animal"
+ ],
+ "moji": "🦀"
},
"crayon": {
"unicode": "1F58D",
"unicode_alternates": [],
"name": "lower left crayon",
"shortname": ":crayon:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":lower_left_crayon:"
],
@@ -5562,8 +6119,11 @@
"write",
"draw",
"color",
- "wax"
- ]
+ "wax",
+ "object",
+ "office"
+ ],
+ "moji": "🖍"
},
"credit_card": {
"unicode": "1F4B3",
@@ -5588,7 +6148,9 @@
"visa",
"american express",
"wallet",
- "signature"
+ "signature",
+ "object",
+ "boys night"
],
"moji": "💳"
},
@@ -5606,15 +6168,16 @@
"crescent",
"waxing",
"sky",
- "night",
"cheese",
- "phase"
+ "phase",
+ "space",
+ "goodnight"
],
"moji": "🌙"
},
"cricket": {
"unicode": "1F3CF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "cricket bat and ball",
"shortname": ":cricket:",
"category": "activity",
@@ -5622,7 +6185,12 @@
":cricket_bat_ball:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "ball",
+ "sport",
+ "cricket"
+ ],
+ "moji": "🏏"
},
"crocodile": {
"unicode": "1F40A",
@@ -5639,13 +6207,26 @@
"croc",
"alligator",
"gator",
- "cranky"
+ "cranky",
+ "wildlife",
+ "reptile"
],
"moji": "🐊"
},
+ "croissant": {
+ "unicode": "1F950",
+ "unicode_alternates": [],
+ "name": "croissant",
+ "shortname": ":croissant:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥐"
+ },
"cross": {
"unicode": "271D",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "latin cross",
"shortname": ":cross:",
"category": "symbols",
@@ -5657,53 +6238,8 @@
"religion",
"symbol",
"christian"
- ]
- },
- "cross_heavy": {
- "unicode": "1F547",
- "unicode_alternates": [],
- "name": "heavy latin cross",
- "shortname": ":cross_heavy:",
- "category": "objects_symbols",
- "aliases": [
- ":heavy_latin_cross:"
],
- "aliases_ascii": [],
- "keywords": [
- "religion",
- "symbol"
- ]
- },
- "cross_white": {
- "unicode": "1F546",
- "unicode_alternates": [],
- "name": "white latin cross",
- "shortname": ":cross_white:",
- "category": "objects_symbols",
- "aliases": [
- ":white_latin_cross:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "religion",
- "symbol"
- ]
- },
- "crossbones": {
- "unicode": "1F571",
- "unicode_alternates": [],
- "name": "black skull and crossbones",
- "shortname": ":crossbones:",
- "category": "objects_symbols",
- "aliases": [
- ":black_skull_and_crossbones:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "poison",
- "danger",
- "death"
- ]
+ "moji": "✝"
},
"crossed_flags": {
"unicode": "1F38C",
@@ -5714,13 +6250,14 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "japan"
+ "japan",
+ "object"
],
"moji": "🎌"
},
"crossed_swords": {
"unicode": "2694",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "crossed swords",
"shortname": ":crossed_swords:",
"category": "objects",
@@ -5729,21 +6266,25 @@
"keywords": [
"object",
"weapon"
- ]
+ ],
+ "moji": "⚔"
},
"crown": {
"unicode": "1F451",
"unicode_alternates": [],
"name": "crown",
"shortname": ":crown:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"king",
"kod",
"leader",
- "royalty"
+ "royalty",
+ "object",
+ "gem",
+ "accessories"
],
"moji": "👑"
},
@@ -5752,7 +6293,7 @@
"unicode_alternates": [],
"name": "passenger ship",
"shortname": ":cruise_ship:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":passenger_ship:"
],
@@ -5760,15 +6301,18 @@
"keywords": [
"titanic",
"transportation",
- "boat"
- ]
+ "boat",
+ "travel",
+ "vacation"
+ ],
+ "moji": "🛳"
},
"cry": {
"unicode": "1F622",
"unicode_alternates": [],
"name": "crying face",
"shortname": ":cry:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":'(",
@@ -5779,11 +6323,14 @@
"keywords": [
"face",
"sad",
- "sad",
"cry",
"tear",
"weep",
- "tears"
+ "tears",
+ "smiley",
+ "emotion",
+ "rip",
+ "heartbreak"
],
"moji": "😢"
},
@@ -5792,7 +6339,7 @@
"unicode_alternates": [],
"name": "crying cat face",
"shortname": ":crying_cat_face:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5804,8 +6351,6 @@
"cry",
"cat",
"sob",
- "tears",
- "sad",
"melancholy",
"morn",
"somber",
@@ -5823,16 +6368,29 @@
"aliases_ascii": [],
"keywords": [
"disco",
- "party"
+ "party",
+ "object",
+ "ball"
],
"moji": "🔮"
},
+ "cucumber": {
+ "unicode": "1F952",
+ "unicode_alternates": [],
+ "name": "cucumber",
+ "shortname": ":cucumber:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥒"
+ },
"cupid": {
"unicode": "1F498",
"unicode_alternates": [],
"name": "heart with arrow",
"shortname": ":cupid:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5840,7 +6398,8 @@
"heart",
"like",
"love",
- "valentines"
+ "valentines",
+ "symbol"
],
"moji": "💘"
},
@@ -5849,11 +6408,12 @@
"unicode_alternates": [],
"name": "curly loop",
"shortname": ":curly_loop:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "scribble"
+ "scribble",
+ "symbol"
],
"moji": "➰"
},
@@ -5862,13 +6422,14 @@
"unicode_alternates": [],
"name": "currency exchange",
"shortname": ":currency_exchange:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"dollar",
"money",
- "travel"
+ "travel",
+ "symbol"
],
"moji": "💱"
},
@@ -5877,7 +6438,7 @@
"unicode_alternates": [],
"name": "curry and rice",
"shortname": ":curry:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5888,7 +6449,6 @@
"curry",
"spice",
"flavor",
- "food",
"meal"
],
"moji": "🍛"
@@ -5898,7 +6458,7 @@
"unicode_alternates": [],
"name": "custard",
"shortname": ":custard:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5920,7 +6480,7 @@
"unicode_alternates": [],
"name": "customs",
"shortname": ":customs:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5932,7 +6492,8 @@
"goods",
"check",
"authority",
- "government"
+ "government",
+ "symbol"
],
"moji": "🛃"
},
@@ -5942,7 +6503,7 @@
"unicode_alternates": [],
"name": "cyclone",
"shortname": ":cyclone:",
- "category": "nature",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5954,7 +6515,9 @@
"hurricane",
"typhoon",
"storm",
- "ocean"
+ "ocean",
+ "symbol",
+ "drugs"
]
},
"dagger": {
@@ -5962,22 +6525,25 @@
"unicode_alternates": [],
"name": "dagger knife",
"shortname": ":dagger:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":dagger_knife:"
],
"aliases_ascii": [],
"keywords": [
"blade",
- "knife"
- ]
+ "knife",
+ "object",
+ "weapon"
+ ],
+ "moji": "🗡"
},
"dancer": {
"unicode": "1F483",
"unicode_alternates": [],
"name": "dancer",
"shortname": ":dancer:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -5995,13 +6561,18 @@
"ballet",
"tango",
"cha cha",
- "music"
+ "music",
+ "people",
+ "women",
+ "sexy",
+ "diversity",
+ "girls night"
],
"moji": "💃"
},
"dancer_tone1": {
"unicode": "1F483-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "dancer tone 1",
"shortname": ":dancer_tone1:",
"category": "people",
@@ -6021,11 +6592,12 @@
"tango",
"cha cha",
"music"
- ]
+ ],
+ "moji": "💃🏻"
},
"dancer_tone2": {
"unicode": "1F483-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "dancer tone 2",
"shortname": ":dancer_tone2:",
"category": "people",
@@ -6045,11 +6617,12 @@
"tango",
"cha cha",
"music"
- ]
+ ],
+ "moji": "💃🏼"
},
"dancer_tone3": {
"unicode": "1F483-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "dancer tone 3",
"shortname": ":dancer_tone3:",
"category": "people",
@@ -6069,11 +6642,12 @@
"tango",
"cha cha",
"music"
- ]
+ ],
+ "moji": "💃🏽"
},
"dancer_tone4": {
"unicode": "1F483-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "dancer tone 4",
"shortname": ":dancer_tone4:",
"category": "people",
@@ -6093,11 +6667,12 @@
"tango",
"cha cha",
"music"
- ]
+ ],
+ "moji": "💃🏾"
},
"dancer_tone5": {
"unicode": "1F483-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "dancer tone 5",
"shortname": ":dancer_tone5:",
"category": "people",
@@ -6117,14 +6692,15 @@
"tango",
"cha cha",
"music"
- ]
+ ],
+ "moji": "💃🏿"
},
"dancers": {
"unicode": "1F46F",
"unicode_alternates": [],
"name": "woman with bunny ears",
"shortname": ":dancers:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6137,8 +6713,13 @@
"showgirl",
"playboy",
"costume",
- "bunny",
- "cancan"
+ "cancan",
+ "people",
+ "sexy",
+ "girls night",
+ "boys night",
+ "parties",
+ "dance"
],
"moji": "👯"
},
@@ -6147,7 +6728,7 @@
"unicode_alternates": [],
"name": "dango",
"shortname": ":dango:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6166,20 +6747,24 @@
"unicode_alternates": [],
"name": "dark sunglasses",
"shortname": ":dark_sunglasses:",
- "category": "objects_symbols",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"shades",
- "eyes"
- ]
+ "eyes",
+ "fashion",
+ "glasses",
+ "accessories"
+ ],
+ "moji": "🕶"
},
"dart": {
"unicode": "1F3AF",
"unicode_alternates": [],
"name": "direct hit",
"shortname": ":dart:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6190,10 +6775,10 @@
"bullseye",
"dart",
"archery",
- "game",
"fletching",
"arrow",
- "sport"
+ "sport",
+ "boys night"
],
"moji": "🎯"
},
@@ -6202,14 +6787,17 @@
"unicode_alternates": [],
"name": "dash symbol",
"shortname": ":dash:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"air",
"fast",
"shoo",
- "wind"
+ "wind",
+ "cloud",
+ "cold",
+ "smoking"
],
"moji": "💨"
},
@@ -6223,7 +6811,9 @@
"aliases_ascii": [],
"keywords": [
"calendar",
- "schedule"
+ "schedule",
+ "object",
+ "office"
],
"moji": "📅"
},
@@ -6242,16 +6832,29 @@
"tree",
"leaves",
"fall",
- "color"
+ "color",
+ "camp",
+ "trees"
],
"moji": "🌳"
},
+ "deer": {
+ "unicode": "1F98C",
+ "unicode_alternates": [],
+ "name": "deer",
+ "shortname": ":deer:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦌"
+ },
"department_store": {
"unicode": "1F3EC",
"unicode_alternates": [],
"name": "department store",
"shortname": ":department_store:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6262,31 +6865,17 @@
"store",
"retail",
"sale",
- "merchandise"
+ "merchandise",
+ "places"
],
"moji": "🏬"
},
- "descending_notes": {
- "unicode": "1F39D",
- "unicode_alternates": [],
- "name": "beamed descending musical notes",
- "shortname": ":descending_notes:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "score",
- "music",
- "sound",
- "tone"
- ]
- },
"desert": {
"unicode": "1F3DC",
"unicode_alternates": [],
"name": "desert",
"shortname": ":desert:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6295,41 +6884,36 @@
"sandy",
"cactus",
"sunny",
- "barren"
- ]
+ "barren",
+ "places",
+ "travel",
+ "vacation"
+ ],
+ "moji": "🏜"
},
"desktop": {
"unicode": "1F5A5",
"unicode_alternates": [],
"name": "desktop computer",
"shortname": ":desktop:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":desktop_computer:"
],
"aliases_ascii": [],
"keywords": [
- "cpu"
- ]
- },
- "desktop_window": {
- "unicode": "1F5D4",
- "unicode_alternates": [],
- "name": "desktop window",
- "shortname": ":desktop_window:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "computer"
- ]
+ "cpu",
+ "electronics",
+ "work"
+ ],
+ "moji": "🖥"
},
"diamond_shape_with_a_dot_inside": {
"unicode": "1F4A0",
"unicode_alternates": [],
"name": "diamond shape with a dot inside",
"shortname": ":diamond_shape_with_a_dot_inside:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6339,7 +6923,8 @@
"kawaii",
"japanese",
"glyph",
- "adorable"
+ "adorable",
+ "symbol"
],
"moji": "💠"
},
@@ -6350,12 +6935,15 @@
],
"name": "black diamond suit",
"shortname": ":diamonds:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cards",
- "poker"
+ "poker",
+ "shapes",
+ "symbol",
+ "game"
],
"moji": "♦"
},
@@ -6364,7 +6952,7 @@
"unicode_alternates": [],
"name": "disappointed face",
"shortname": ":disappointed:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
">:[",
@@ -6382,7 +6970,10 @@
"discouraged",
"face",
"sad",
- "upset"
+ "upset",
+ "smiley",
+ "tired",
+ "emotion"
],
"moji": "😞"
},
@@ -6391,7 +6982,7 @@
"unicode_alternates": [],
"name": "disappointed but relieved face",
"shortname": ":disappointed_relieved:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6400,7 +6991,12 @@
"phew",
"sweat",
"disappoint",
- "relief"
+ "relief",
+ "sad",
+ "smiley",
+ "stressed",
+ "cry",
+ "emotion"
],
"moji": "😥"
},
@@ -6409,22 +7005,25 @@
"unicode_alternates": [],
"name": "card index dividers",
"shortname": ":dividers:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":card_index_dividers:"
],
"aliases_ascii": [],
"keywords": [
"stationery",
- "rolodex"
- ]
+ "rolodex",
+ "work",
+ "office"
+ ],
+ "moji": "🗂"
},
"dizzy": {
"unicode": "1F4AB",
"unicode_alternates": [],
"name": "dizzy symbol",
"shortname": ":dizzy:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6437,7 +7036,7 @@
"intoxicated",
"squeans",
"starburst",
- "star"
+ "symbol"
],
"moji": "💫"
},
@@ -6446,7 +7045,7 @@
"unicode_alternates": [],
"name": "dizzy face",
"shortname": ":dizzy_face:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"#-)",
@@ -6463,7 +7062,13 @@
"face",
"spent",
"unconscious",
- "xox"
+ "xox",
+ "smiley",
+ "surprised",
+ "dead",
+ "wow",
+ "emotion",
+ "omg"
],
"moji": "😵"
},
@@ -6472,7 +7077,7 @@
"unicode_alternates": [],
"name": "do not litter symbol",
"shortname": ":do_not_litter:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6480,40 +7085,13 @@
"garbage",
"trash",
"litter",
- "garbage",
"waste",
"no",
"can",
- "trash"
+ "symbol"
],
"moji": "🚯"
},
- "document": {
- "unicode": "1F5CE",
- "unicode_alternates": [],
- "name": "document",
- "shortname": ":document:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "page"
- ]
- },
- "document_text": {
- "unicode": "1F5B9",
- "unicode_alternates": [],
- "name": "document with text",
- "shortname": ":document_text:",
- "category": "objects_symbols",
- "aliases": [
- ":document_with_text:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "page"
- ]
- },
"dog": {
"unicode": "1F436",
"unicode_alternates": [],
@@ -6526,7 +7104,9 @@
"animal",
"friend",
"nature",
- "woof"
+ "woof",
+ "dog",
+ "pug"
],
"moji": "🐶"
},
@@ -6546,11 +7126,10 @@
"pet",
"dog",
"puppy",
- "pet",
- "friend",
"woof",
"bark",
- "fido"
+ "fido",
+ "pug"
],
"moji": "🐕"
},
@@ -6571,8 +7150,6 @@
"canada",
"australia",
"banknote",
- "money",
- "currency",
"paper",
"cash",
"bills"
@@ -6593,7 +7170,6 @@
"toy",
"dolls",
"japan",
- "japanese",
"day",
"girls",
"emperor",
@@ -6602,7 +7178,8 @@
"blessing",
"imperial",
"family",
- "royal"
+ "royal",
+ "people"
],
"moji": "🎎"
},
@@ -6621,7 +7198,9 @@
"flipper",
"nature",
"ocean",
- "sea"
+ "sea",
+ "wildlife",
+ "tropical"
],
"moji": "🐬"
},
@@ -6641,8 +7220,7 @@
"doorway",
"entrance",
"enter",
- "exit",
- "entry"
+ "object"
],
"moji": "🚪"
},
@@ -6651,7 +7229,7 @@
"unicode_alternates": [],
"name": "doughnut",
"shortname": ":doughnut:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6666,8 +7244,7 @@
"dessert",
"breakfast",
"police",
- "homer",
- "sweet"
+ "homer"
],
"moji": "🍩"
},
@@ -6676,15 +7253,17 @@
"unicode_alternates": [],
"name": "dove of peace",
"shortname": ":dove:",
- "category": "objects_symbols",
+ "category": "nature",
"aliases": [
":dove_of_peace:"
],
"aliases_ascii": [],
"keywords": [
"symbol",
- "bird"
- ]
+ "bird",
+ "animal"
+ ],
+ "moji": "🕊"
},
"dragon": {
"unicode": "1F409",
@@ -6703,7 +7282,8 @@
"dragon",
"fire",
"legendary",
- "myth"
+ "roar",
+ "reptile"
],
"moji": "🐉"
},
@@ -6725,7 +7305,9 @@
"head",
"fire",
"legendary",
- "myth"
+ "roar",
+ "monster",
+ "reptile"
],
"moji": "🐲"
},
@@ -6734,12 +7316,15 @@
"unicode_alternates": [],
"name": "dress",
"shortname": ":dress:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"clothes",
- "fashion"
+ "fashion",
+ "women",
+ "sexy",
+ "girls night"
],
"moji": "👗"
},
@@ -6758,23 +7343,35 @@
"dromedary",
"camel",
"hump",
- "desert",
"middle east",
"heat",
- "hot",
"water",
"hump day",
"wednesday",
- "sex"
+ "sex",
+ "wildlife"
],
"moji": "🐪"
},
+ "drooling_face": {
+ "unicode": "1F924",
+ "unicode_alternates": [],
+ "name": "drooling face",
+ "shortname": ":drooling_face:",
+ "category": "people",
+ "aliases": [
+ ":drool:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤤"
+ },
"droplet": {
"unicode": "1F4A7",
"unicode_alternates": [],
"name": "droplet",
"shortname": ":droplet:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -6784,7 +7381,6 @@
"drop",
"droplet",
"h20",
- "water",
"aqua",
"tear",
"sweat",
@@ -6792,10 +7388,36 @@
"moisture",
"wet",
"moist",
- "spit"
+ "spit",
+ "weather",
+ "sky"
],
"moji": "💧"
},
+ "drum": {
+ "unicode": "1F941",
+ "unicode_alternates": [],
+ "name": "drum with drumsticks",
+ "shortname": ":drum:",
+ "category": "activity",
+ "aliases": [
+ ":drum_with_drumsticks:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥁"
+ },
+ "duck": {
+ "unicode": "1F986",
+ "unicode_alternates": [],
+ "name": "duck",
+ "shortname": ":duck:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦆"
+ },
"dvd": {
"unicode": "1F4C0",
"unicode_alternates": [],
@@ -6807,7 +7429,8 @@
"keywords": [
"cd",
"disc",
- "disk"
+ "disk",
+ "electronics"
],
"moji": "📀"
},
@@ -6823,23 +7446,37 @@
"aliases_ascii": [],
"keywords": [
"communication",
- "inbox"
+ "inbox",
+ "office"
],
"moji": "📧"
},
+ "eagle": {
+ "unicode": "1F985",
+ "unicode_alternates": [],
+ "name": "eagle",
+ "shortname": ":eagle:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦅"
+ },
"ear": {
"unicode": "1F442",
"unicode_alternates": [],
"name": "ear",
"shortname": ":ear:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"face",
"hear",
"listen",
- "sound"
+ "sound",
+ "body",
+ "diversity"
],
"moji": "👂"
},
@@ -6857,14 +7494,14 @@
"ear",
"rice",
"food",
- "plant",
- "seed"
+ "seed",
+ "leaf"
],
"moji": "🌾"
},
"ear_tone1": {
"unicode": "1F442-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ear tone 1",
"shortname": ":ear_tone1:",
"category": "people",
@@ -6874,11 +7511,12 @@
"hear",
"listen",
"sound"
- ]
+ ],
+ "moji": "👂🏻"
},
"ear_tone2": {
"unicode": "1F442-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ear tone 2",
"shortname": ":ear_tone2:",
"category": "people",
@@ -6888,11 +7526,12 @@
"hear",
"listen",
"sound"
- ]
+ ],
+ "moji": "👂🏼"
},
"ear_tone3": {
"unicode": "1F442-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ear tone 3",
"shortname": ":ear_tone3:",
"category": "people",
@@ -6902,11 +7541,12 @@
"hear",
"listen",
"sound"
- ]
+ ],
+ "moji": "👂🏽"
},
"ear_tone4": {
"unicode": "1F442-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ear tone 4",
"shortname": ":ear_tone4:",
"category": "people",
@@ -6916,11 +7556,12 @@
"hear",
"listen",
"sound"
- ]
+ ],
+ "moji": "👂🏾"
},
"ear_tone5": {
"unicode": "1F442-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ear tone 5",
"shortname": ":ear_tone5:",
"category": "people",
@@ -6930,7 +7571,8 @@
"hear",
"listen",
"sound"
- ]
+ ],
+ "moji": "👂🏿"
},
"earth_africa": {
"unicode": "1F30D",
@@ -6945,12 +7587,13 @@
"international",
"world",
"earth",
- "globe",
"space",
"planet",
"africa",
"europe",
- "home"
+ "home",
+ "map",
+ "vacation"
],
"moji": "🌍"
},
@@ -6968,14 +7611,15 @@
"international",
"world",
"earth",
- "globe",
"space",
"planet",
"north",
"south",
"america",
"americas",
- "home"
+ "home",
+ "map",
+ "vacation"
],
"moji": "🌎"
},
@@ -6993,43 +7637,33 @@
"international",
"world",
"earth",
- "globe",
"space",
"planet",
"asia",
"australia",
- "home"
+ "home",
+ "map",
+ "vacation"
],
"moji": "🌏"
},
"egg": {
- "unicode": "1F373",
+ "unicode": "1F95A",
"unicode_alternates": [],
- "name": "cooking",
+ "name": "egg",
"shortname": ":egg:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
- "keywords": [
- "breakfast",
- "food",
- "egg",
- "fry",
- "pan",
- "flat",
- "cook",
- "frying",
- "cooking",
- "utensil"
- ],
- "moji": "🍳"
+ "keywords": [],
+ "moji": "🥚"
},
"eggplant": {
"unicode": "1F346",
"unicode_alternates": [],
"name": "aubergine",
"shortname": ":eggplant:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7038,10 +7672,10 @@
"nature",
"vegetable",
"eggplant",
- "aubergine",
"fruit",
"purple",
- "penis"
+ "penis",
+ "vegetables"
],
"moji": "🍆"
},
@@ -7051,15 +7685,18 @@
"unicode_alternates": [
"0038-FE0F-20E3"
],
- "name": "digit eight",
+ "name": "keycap digit eight",
"shortname": ":eight:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"8",
"blue-square",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"eight_pointed_black_star": {
@@ -7069,10 +7706,12 @@
],
"name": "eight pointed black star",
"shortname": ":eight_pointed_black_star:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "symbol"
+ ],
"moji": "✴"
},
"eight_spoked_asterisk": {
@@ -7082,16 +7721,32 @@
],
"name": "eight spoked asterisk",
"shortname": ":eight_spoked_asterisk:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"green-square",
"sparkle",
- "star"
+ "star",
+ "symbol"
],
"moji": "✳"
},
+ "eject": {
+ "unicode": "23CF",
+ "unicode_alternates": [
+ "23CF-FE0F"
+ ],
+ "name": "eject symbol",
+ "shortname": ":eject:",
+ "category": "symbols",
+ "aliases": [
+ ":eject_symbol:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "⏏"
+ },
"electric_plug": {
"unicode": "1F50C",
"unicode_alternates": [],
@@ -7102,7 +7757,8 @@
"aliases_ascii": [],
"keywords": [
"charger",
- "power"
+ "power",
+ "electronics"
],
"moji": "🔌"
},
@@ -7118,7 +7774,8 @@
"animal",
"nature",
"nose",
- "thailand"
+ "thailand",
+ "wildlife"
],
"moji": "🐘"
},
@@ -7127,12 +7784,13 @@
"unicode_alternates": [],
"name": "end with leftwards arrow above",
"shortname": ":end:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "words"
+ "words",
+ "symbol"
],
"moji": "🔚"
},
@@ -7150,78 +7808,13 @@
"communication",
"letter",
"mail",
- "postal"
+ "postal",
+ "object",
+ "office",
+ "write"
],
"moji": "✉"
},
- "envelope_back": {
- "unicode": "1F582",
- "unicode_alternates": [],
- "name": "back of envelope",
- "shortname": ":envelope_back:",
- "category": "objects_symbols",
- "aliases": [
- ":back_of_envelope:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "letter",
- "mail",
- "postal"
- ]
- },
- "envelope_flying": {
- "unicode": "1F585",
- "unicode_alternates": [],
- "name": "flying envelope",
- "shortname": ":envelope_flying:",
- "category": "objects_symbols",
- "aliases": [
- ":flying_envelope:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "letter",
- "mail",
- "postal"
- ]
- },
- "envelope_stamped": {
- "unicode": "1F583",
- "unicode_alternates": [],
- "name": "stamped envelope",
- "shortname": ":envelope_stamped:",
- "category": "objects_symbols",
- "aliases": [
- ":stamped_envelope:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "letter",
- "mail",
- "postal"
- ]
- },
- "envelope_stamped_pen": {
- "unicode": "1F586",
- "unicode_alternates": [],
- "name": "pen over stamped envelope",
- "shortname": ":envelope_stamped_pen:",
- "category": "objects_symbols",
- "aliases": [
- ":pen_over_stamped_envelope:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "letter",
- "mail",
- "postal"
- ]
- },
"envelope_with_arrow": {
"unicode": "1F4E9",
"unicode_alternates": [],
@@ -7231,7 +7824,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "email"
+ "email",
+ "object",
+ "office"
],
"moji": "📩"
},
@@ -7250,8 +7845,6 @@
"euro",
"europe",
"banknote",
- "money",
- "currency",
"paper",
"cash",
"bills"
@@ -7263,7 +7856,7 @@
"unicode_alternates": [],
"name": "european castle",
"shortname": ":european_castle:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7273,7 +7866,6 @@
"castle",
"european",
"residence",
- "royalty",
"disneyland",
"disney",
"fort",
@@ -7287,7 +7879,10 @@
"queen",
"fortress",
"nobel",
- "stronghold"
+ "stronghold",
+ "places",
+ "travel",
+ "vacation"
],
"moji": "🏰"
},
@@ -7296,11 +7891,13 @@
"unicode_alternates": [],
"name": "european post office",
"shortname": ":european_post_office:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "building"
+ "building",
+ "places",
+ "post office"
],
"moji": "🏤"
},
@@ -7318,7 +7915,10 @@
"evergreen",
"tree",
"needles",
- "christmas"
+ "christmas",
+ "holidays",
+ "camp",
+ "trees"
],
"moji": "🌲"
},
@@ -7329,11 +7929,13 @@
],
"name": "heavy exclamation mark symbol",
"shortname": ":exclamation:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "surprise"
+ "surprise",
+ "symbol",
+ "punctuation"
],
"moji": "❗"
},
@@ -7342,7 +7944,7 @@
"unicode_alternates": [],
"name": "expressionless face",
"shortname": ":expressionless:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"-_-",
@@ -7356,7 +7958,11 @@
"vapid",
"without expression",
"face",
- "indifferent"
+ "indifferent",
+ "mad",
+ "smiley",
+ "neutral",
+ "emotion"
],
"moji": "😑"
},
@@ -7371,25 +7977,36 @@
"keywords": [
"look",
"peek",
- "watch"
- ]
+ "watch",
+ "body",
+ "eyes"
+ ],
+ "moji": "👁"
},
"eye_in_speech_bubble": {
"unicode": "1F441-1F5E8",
- "unicode_alternates": "1f441-200d-1f5e8",
+ "unicode_alternates": [
+ "1F441-200D-1F5E8"
+ ],
"name": "eye in speech bubble",
"shortname": ":eye_in_speech_bubble:",
"category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "object",
+ "symbol",
+ "eyes",
+ "talk"
+ ],
+ "moji": "👁‍🗨"
},
"eyeglasses": {
"unicode": "1F453",
"unicode_alternates": [],
"name": "eyeglasses",
"shortname": ":eyeglasses:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7408,7 +8025,8 @@
"vision",
"see",
"blurry",
- "contacts"
+ "contacts",
+ "glasses"
],
"moji": "👓"
},
@@ -7417,27 +8035,110 @@
"unicode_alternates": [],
"name": "eyes",
"shortname": ":eyes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"look",
"peek",
"stalk",
- "watch"
+ "watch",
+ "body",
+ "eyes"
],
"moji": "👀"
},
+ "face_palm": {
+ "unicode": "1F926",
+ "unicode_alternates": [],
+ "name": "face palm",
+ "shortname": ":face_palm:",
+ "category": "people",
+ "aliases": [
+ ":facepalm:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦"
+ },
+ "face_palm_tone1": {
+ "unicode": "1F926-1F3FB",
+ "unicode_alternates": [],
+ "name": "face palm tone 1",
+ "shortname": ":face_palm_tone1:",
+ "category": "people",
+ "aliases": [
+ ":facepalm_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦🏻"
+ },
+ "face_palm_tone2": {
+ "unicode": "1F926-1F3FC",
+ "unicode_alternates": [],
+ "name": "face palm tone 2",
+ "shortname": ":face_palm_tone2:",
+ "category": "people",
+ "aliases": [
+ ":facepalm_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦🏼"
+ },
+ "face_palm_tone3": {
+ "unicode": "1F926-1F3FD",
+ "unicode_alternates": [],
+ "name": "face palm tone 3",
+ "shortname": ":face_palm_tone3:",
+ "category": "people",
+ "aliases": [
+ ":facepalm_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦🏽"
+ },
+ "face_palm_tone4": {
+ "unicode": "1F926-1F3FE",
+ "unicode_alternates": [],
+ "name": "face palm tone 4",
+ "shortname": ":face_palm_tone4:",
+ "category": "people",
+ "aliases": [
+ ":facepalm_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦🏾"
+ },
+ "face_palm_tone5": {
+ "unicode": "1F926-1F3FF",
+ "unicode_alternates": [],
+ "name": "face palm tone 5",
+ "shortname": ":face_palm_tone5:",
+ "category": "people",
+ "aliases": [
+ ":facepalm_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤦🏿"
+ },
"factory": {
"unicode": "1F3ED",
"unicode_alternates": [],
"name": "factory",
"shortname": ":factory:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "building"
+ "building",
+ "places",
+ "travel",
+ "steam"
],
"moji": "🏭"
},
@@ -7467,7 +8168,7 @@
"unicode_alternates": [],
"name": "family",
"shortname": ":family:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7479,13 +8180,12 @@
"mother",
"parents",
"family",
- "mother",
- "father",
- "child",
"girl",
"boy",
"group",
- "unit"
+ "unit",
+ "people",
+ "baby"
],
"moji": "👪"
},
@@ -7509,8 +8209,14 @@
"gay",
"homosexual",
"man",
- "boy"
- ]
+ "boy",
+ "people",
+ "family",
+ "men",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👨‍👨‍👦"
},
"family_mmbb": {
"unicode": "1F468-1F468-1F466-1F466",
@@ -7532,8 +8238,14 @@
"gay",
"homosexual",
"man",
- "boy"
- ]
+ "boy",
+ "people",
+ "family",
+ "men",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👨‍👨‍👦‍👦"
},
"family_mmg": {
"unicode": "1F468-1F468-1F467",
@@ -7555,8 +8267,14 @@
"gay",
"homosexual",
"man",
- "girl"
- ]
+ "girl",
+ "people",
+ "family",
+ "men",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👨‍👨‍👧"
},
"family_mmgb": {
"unicode": "1F468-1F468-1F467-1F466",
@@ -7579,8 +8297,14 @@
"homosexual",
"man",
"girl",
- "boy"
- ]
+ "boy",
+ "people",
+ "family",
+ "men",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👨‍👨‍👧‍👦"
},
"family_mmgg": {
"unicode": "1F468-1F468-1F467-1F467",
@@ -7602,8 +8326,14 @@
"gay",
"homosexual",
"man",
- "girl"
- ]
+ "girl",
+ "people",
+ "family",
+ "men",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👨‍👨‍👧‍👧"
},
"family_mwbb": {
"unicode": "1F468-1F469-1F466-1F466",
@@ -7626,8 +8356,12 @@
"group",
"unit",
"man",
- "woman"
- ]
+ "woman",
+ "people",
+ "family",
+ "baby"
+ ],
+ "moji": "👨‍👩‍👦‍👦"
},
"family_mwg": {
"unicode": "1F468-1F469-1F467",
@@ -7651,8 +8385,12 @@
"group",
"unit",
"man",
- "woman"
- ]
+ "woman",
+ "people",
+ "family",
+ "baby"
+ ],
+ "moji": "👨‍👩‍👧"
},
"family_mwgb": {
"unicode": "1F468-1F469-1F467-1F466",
@@ -7676,8 +8414,12 @@
"group",
"unit",
"man",
- "woman"
- ]
+ "woman",
+ "people",
+ "family",
+ "baby"
+ ],
+ "moji": "👨‍👩‍👧‍👦"
},
"family_mwgg": {
"unicode": "1F468-1F469-1F467-1F467",
@@ -7700,8 +8442,12 @@
"group",
"unit",
"man",
- "woman"
- ]
+ "woman",
+ "people",
+ "family",
+ "baby"
+ ],
+ "moji": "👨‍👩‍👧‍👧"
},
"family_wwb": {
"unicode": "1F469-1F469-1F466",
@@ -7724,8 +8470,14 @@
"gay",
"lesbian",
"homosexual",
- "woman"
- ]
+ "woman",
+ "people",
+ "family",
+ "women",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👩‍👩‍👦"
},
"family_wwbb": {
"unicode": "1F469-1F469-1F466-1F466",
@@ -7748,8 +8500,14 @@
"lesbian",
"homosexual",
"woman",
- "boy"
- ]
+ "boy",
+ "people",
+ "family",
+ "women",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👩‍👩‍👦‍👦"
},
"family_wwg": {
"unicode": "1F469-1F469-1F467",
@@ -7772,8 +8530,14 @@
"unit",
"gay",
"lesbian",
- "homosexual"
- ]
+ "homosexual",
+ "people",
+ "family",
+ "women",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👩‍👩‍👧"
},
"family_wwgb": {
"unicode": "1F469-1F469-1F467-1F466",
@@ -7797,8 +8561,14 @@
"homosexual",
"woman",
"girl",
- "boy"
- ]
+ "boy",
+ "people",
+ "family",
+ "women",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👩‍👩‍👧‍👦"
},
"family_wwgg": {
"unicode": "1F469-1F469-1F467-1F467",
@@ -7821,19 +8591,27 @@
"lesbian",
"homosexual",
"woman",
- "girl"
- ]
+ "girl",
+ "people",
+ "family",
+ "women",
+ "baby",
+ "lgbt"
+ ],
+ "moji": "👩‍👩‍👧‍👧"
},
"fast_forward": {
"unicode": "23E9",
"unicode_alternates": [],
"name": "black right-pointing double triangle",
"shortname": ":fast_forward:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "⏩"
},
@@ -7847,7 +8625,10 @@
"aliases_ascii": [],
"keywords": [
"communication",
- "technology"
+ "technology",
+ "electronics",
+ "work",
+ "office"
],
"moji": "📠"
},
@@ -7856,7 +8637,7 @@
"unicode_alternates": [],
"name": "fearful face",
"shortname": ":fearful:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7867,8 +8648,10 @@
"terrified",
"fear",
"fearful",
- "scared",
- "frightened"
+ "frightened",
+ "smiley",
+ "surprised",
+ "emotion"
],
"moji": "😨"
},
@@ -7888,16 +8671,12 @@
"paw",
"pet",
"tracking",
- "paw",
"prints",
"mark",
"imprints",
"footsteps",
- "animal",
"lion",
"bear",
- "dog",
- "cat",
"raccoon",
"critter",
"feet",
@@ -7905,12 +8684,25 @@
],
"moji": "🐾"
},
+ "fencer": {
+ "unicode": "1F93A",
+ "unicode_alternates": [],
+ "name": "fencer",
+ "shortname": ":fencer:",
+ "category": "activity",
+ "aliases": [
+ ":fencing:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤺"
+ },
"ferris_wheel": {
"unicode": "1F3A1",
"unicode_alternates": [],
"name": "ferris wheel",
"shortname": ":ferris_wheel:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7923,13 +8715,16 @@
"park",
"fair",
"ride",
- "entertainment"
+ "entertainment",
+ "places",
+ "vacation",
+ "ferris wheel"
],
"moji": "🎡"
},
"ferry": {
"unicode": "26F4",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ferry",
"shortname": ":ferry:",
"category": "travel",
@@ -7938,33 +8733,44 @@
"keywords": [
"boat",
"place",
- "travel"
- ]
+ "travel",
+ "transportation",
+ "vacation"
+ ],
+ "moji": "⛴"
},
"field_hockey": {
"unicode": "1F3D1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "field hockey stick and ball",
"shortname": ":field_hockey:",
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "ball",
+ "sport",
+ "hockey"
+ ],
+ "moji": "🏑"
},
"file_cabinet": {
"unicode": "1F5C4",
"unicode_alternates": [],
"name": "file cabinet",
"shortname": ":file_cabinet:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"folders",
"office",
"documents",
- "storage"
- ]
+ "storage",
+ "object",
+ "work"
+ ],
+ "moji": "🗄"
},
"file_folder": {
"unicode": "1F4C1",
@@ -7975,7 +8781,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "documents"
+ "documents",
+ "work",
+ "office"
],
"moji": "📁"
},
@@ -7984,7 +8792,7 @@
"unicode_alternates": [],
"name": "film frames",
"shortname": ":film_frames:",
- "category": "activity",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -7993,95 +8801,96 @@
"8mm",
"16mm",
"reel",
- "celluloid"
- ]
+ "celluloid",
+ "object",
+ "camera"
+ ],
+ "moji": "🎞"
},
- "finger_pointing_down": {
- "unicode": "1F597",
+ "fingers_crossed": {
+ "unicode": "1F91E",
"unicode_alternates": [],
- "name": "white down pointing left hand index",
- "shortname": ":finger_pointing_down:",
+ "name": "hand with first and index finger crossed",
+ "shortname": ":fingers_crossed:",
"category": "people",
"aliases": [
- ":white_down_pointing_left_hand_index:"
+ ":hand_with_index_and_middle_finger_crossed:"
],
"aliases_ascii": [],
- "keywords": [
- "direction",
- "finger",
- "hand"
- ]
+ "keywords": [],
+ "moji": "🤞"
},
- "finger_pointing_down2": {
- "unicode": "1F59F",
+ "fingers_crossed_tone1": {
+ "unicode": "1F91E-1F3FB",
"unicode_alternates": [],
- "name": "sideways white down pointing index",
- "shortname": ":finger_pointing_down2:",
+ "name": "hand with index and middle fingers crossed tone 1",
+ "shortname": ":fingers_crossed_tone1:",
"category": "people",
"aliases": [
- ":sideways_white_down_pointing_index:"
+ ":hand_with_index_and_middle_fingers_crossed_tone1:"
],
"aliases_ascii": [],
- "keywords": [
- "direction",
- "finger",
- "hand"
- ]
+ "keywords": [],
+ "moji": "🤞🏻"
},
- "finger_pointing_left": {
- "unicode": "1F598",
+ "fingers_crossed_tone2": {
+ "unicode": "1F91E-1F3FC",
"unicode_alternates": [],
- "name": "sideways white left pointing index",
- "shortname": ":finger_pointing_left:",
+ "name": "hand with index and middle fingers crossed tone 2",
+ "shortname": ":fingers_crossed_tone2:",
"category": "people",
"aliases": [
- ":sideways_white_left_pointing_index:"
+ ":hand_with_index_and_middle_fingers_crossed_tone2:"
],
"aliases_ascii": [],
- "keywords": [
- "direction",
- "finger",
- "hand"
- ]
+ "keywords": [],
+ "moji": "🤞🏼"
},
- "finger_pointing_right": {
- "unicode": "1F599",
+ "fingers_crossed_tone3": {
+ "unicode": "1F91E-1F3FD",
"unicode_alternates": [],
- "name": "sideways white right pointing index",
- "shortname": ":finger_pointing_right:",
+ "name": "hand with index and middle fingers crossed tone 3",
+ "shortname": ":fingers_crossed_tone3:",
"category": "people",
"aliases": [
- ":sideways_white_right_pointing_index:"
+ ":hand_with_index_and_middle_fingers_crossed_tone3:"
],
"aliases_ascii": [],
- "keywords": [
- "direction",
- "finger",
- "hand"
- ]
+ "keywords": [],
+ "moji": "🤞🏽"
},
- "finger_pointing_up": {
- "unicode": "1F59E",
+ "fingers_crossed_tone4": {
+ "unicode": "1F91E-1F3FE",
"unicode_alternates": [],
- "name": "sideways white up pointing index",
- "shortname": ":finger_pointing_up:",
+ "name": "hand with index and middle fingers crossed tone 4",
+ "shortname": ":fingers_crossed_tone4:",
"category": "people",
"aliases": [
- ":sideways_white_up_pointing_index:"
+ ":hand_with_index_and_middle_fingers_crossed_tone4:"
],
"aliases_ascii": [],
- "keywords": [
- "direction",
- "finger",
- "hand"
- ]
+ "keywords": [],
+ "moji": "🤞🏾"
+ },
+ "fingers_crossed_tone5": {
+ "unicode": "1F91E-1F3FF",
+ "unicode_alternates": [],
+ "name": "hand with index and middle fingers crossed tone 5",
+ "shortname": ":fingers_crossed_tone5:",
+ "category": "people",
+ "aliases": [
+ ":hand_with_index_and_middle_fingers_crossed_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤞🏿"
},
"fire": {
"unicode": "1F525",
"unicode_alternates": [],
"name": "fire",
"shortname": ":fire:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [
":flame:"
],
@@ -8089,7 +8898,8 @@
"keywords": [
"cook",
"hot",
- "flame"
+ "flame",
+ "wth"
],
"moji": "🔥"
},
@@ -8098,7 +8908,7 @@
"unicode_alternates": [],
"name": "fire engine",
"shortname": ":fire_engine:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -8110,34 +8920,17 @@
"engine",
"truck",
"emergency",
- "medical"
+ "medical",
+ "911"
],
"moji": "🚒"
},
- "fire_engine_oncoming": {
- "unicode": "1F6F1",
- "unicode_alternates": [],
- "name": "oncoming fire engine",
- "shortname": ":fire_engine_oncoming:",
- "category": "travel_places",
- "aliases": [
- ":oncoming_fire_engine:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "transportation",
- "vehicle",
- "fighter",
- "truck",
- "emergency"
- ]
- },
"fireworks": {
"unicode": "1F386",
"unicode_alternates": [],
"name": "fireworks",
"shortname": ":fireworks:",
- "category": "objects",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -8154,10 +8947,24 @@
"rocket",
"sky",
"idea",
- "excitement"
+ "excitement",
+ "parties"
],
"moji": "🎆"
},
+ "first_place": {
+ "unicode": "1F947",
+ "unicode_alternates": [],
+ "name": "first place medal",
+ "shortname": ":first_place:",
+ "category": "activity",
+ "aliases": [
+ ":first_place_medal:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥇"
+ },
"first_quarter_moon": {
"unicode": "1F313",
"unicode_alternates": [],
@@ -8174,7 +8981,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌓"
},
@@ -8196,7 +9004,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌛"
},
@@ -8211,7 +9020,8 @@
"keywords": [
"animal",
"food",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐟"
},
@@ -8220,7 +9030,7 @@
"unicode_alternates": [],
"name": "fish cake with swirl design",
"shortname": ":fish_cake:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -8231,7 +9041,8 @@
"swirl",
"ramen",
"noodles",
- "naruto"
+ "naruto",
+ "sushi"
],
"moji": "🍥"
},
@@ -8240,7 +9051,7 @@
"unicode_alternates": [],
"name": "fishing pole and fish",
"shortname": ":fishing_pole_and_fish:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -8248,7 +9059,9 @@
"hobby",
"fish",
"fishing",
- "pole"
+ "pole",
+ "vacation",
+ "sport"
],
"moji": "🎣"
},
@@ -8257,19 +9070,25 @@
"unicode_alternates": [],
"name": "raised fist",
"shortname": ":fist:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fingers",
"grasp",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "hi",
+ "fist bump",
+ "diversity",
+ "condolence"
],
"moji": "✊"
},
"fist_tone1": {
"unicode": "270A-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised fist tone 1",
"shortname": ":fist_tone1:",
"category": "people",
@@ -8279,11 +9098,12 @@
"fingers",
"grasp",
"hand"
- ]
+ ],
+ "moji": "✊🏻"
},
"fist_tone2": {
"unicode": "270A-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised fist tone 2",
"shortname": ":fist_tone2:",
"category": "people",
@@ -8293,11 +9113,12 @@
"fingers",
"grasp",
"hand"
- ]
+ ],
+ "moji": "✊🏼"
},
"fist_tone3": {
"unicode": "270A-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised fist tone 3",
"shortname": ":fist_tone3:",
"category": "people",
@@ -8307,11 +9128,12 @@
"fingers",
"grasp",
"hand"
- ]
+ ],
+ "moji": "✊🏽"
},
"fist_tone4": {
"unicode": "270A-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised fist tone 4",
"shortname": ":fist_tone4:",
"category": "people",
@@ -8321,11 +9143,12 @@
"fingers",
"grasp",
"hand"
- ]
+ ],
+ "moji": "✊🏾"
},
"fist_tone5": {
"unicode": "270A-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised fist tone 5",
"shortname": ":fist_tone5:",
"category": "people",
@@ -8335,7 +9158,8 @@
"fingers",
"grasp",
"hand"
- ]
+ ],
+ "moji": "✊🏿"
},
"five": {
"moji": "5️⃣",
@@ -8343,15 +9167,18 @@
"unicode_alternates": [
"0035-FE0F-20E3"
],
- "name": "digit five",
+ "name": "keycap digit five",
"shortname": ":five:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
"numbers",
- "prime"
+ "prime",
+ "number",
+ "math",
+ "symbol"
]
},
"flag_ac": {
@@ -8367,8 +9194,10 @@
"keywords": [
"country",
"nation",
- "ac"
- ]
+ "ac",
+ "flag"
+ ],
+ "moji": "🇦🇨"
},
"flag_ad": {
"unicode": "1F1E6-1F1E9",
@@ -8383,8 +9212,10 @@
"keywords": [
"country",
"nation",
- "ad"
- ]
+ "ad",
+ "flag"
+ ],
+ "moji": "🇦🇩"
},
"flag_ae": {
"unicode": "1F1E6-1F1EA",
@@ -8399,8 +9230,10 @@
"keywords": [
"country",
"nation",
- "ae"
- ]
+ "ae",
+ "flag"
+ ],
+ "moji": "🇦🇪"
},
"flag_af": {
"unicode": "1F1E6-1F1EB",
@@ -8416,8 +9249,10 @@
"country",
"nation",
"afghanestan",
- "af"
- ]
+ "af",
+ "flag"
+ ],
+ "moji": "🇦🇫"
},
"flag_ag": {
"unicode": "1F1E6-1F1EC",
@@ -8432,8 +9267,10 @@
"keywords": [
"country",
"nation",
- "ag"
- ]
+ "ag",
+ "flag"
+ ],
+ "moji": "🇦🇬"
},
"flag_ai": {
"unicode": "1F1E6-1F1EE",
@@ -8448,8 +9285,10 @@
"keywords": [
"country",
"nation",
- "ai"
- ]
+ "ai",
+ "flag"
+ ],
+ "moji": "🇦🇮"
},
"flag_al": {
"unicode": "1F1E6-1F1F1",
@@ -8465,8 +9304,10 @@
"country",
"nation",
"shqiperia",
- "al"
- ]
+ "al",
+ "flag"
+ ],
+ "moji": "🇦🇱"
},
"flag_am": {
"unicode": "1F1E6-1F1F2",
@@ -8482,8 +9323,10 @@
"country",
"nation",
"hayastan",
- "am"
- ]
+ "am",
+ "flag"
+ ],
+ "moji": "🇦🇲"
},
"flag_ao": {
"unicode": "1F1E6-1F1F4",
@@ -8498,12 +9341,14 @@
"keywords": [
"country",
"nation",
- "ao"
- ]
+ "ao",
+ "flag"
+ ],
+ "moji": "🇦🇴"
},
"flag_aq": {
"unicode": "1F1E6-1F1F6",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "antarctica",
"shortname": ":flag_aq:",
"category": "flags",
@@ -8511,7 +9356,11 @@
":aq:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇦🇶"
},
"flag_ar": {
"unicode": "1F1E6-1F1F7",
@@ -8526,12 +9375,14 @@
"keywords": [
"country",
"nation",
- "ar"
- ]
+ "ar",
+ "flag"
+ ],
+ "moji": "🇦🇷"
},
"flag_as": {
"unicode": "1F1E6-1F1F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "american samoa",
"shortname": ":flag_as:",
"category": "flags",
@@ -8539,7 +9390,11 @@
":as:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇦🇸"
},
"flag_at": {
"unicode": "1F1E6-1F1F9",
@@ -8556,8 +9411,10 @@
"nation",
"&ouml;sterreich",
"osterreich",
- "at"
- ]
+ "at",
+ "flag"
+ ],
+ "moji": "🇦🇹"
},
"flag_au": {
"unicode": "1F1E6-1F1FA",
@@ -8572,8 +9429,10 @@
"keywords": [
"country",
"nation",
- "au"
- ]
+ "au",
+ "flag"
+ ],
+ "moji": "🇦🇺"
},
"flag_aw": {
"unicode": "1F1E6-1F1FC",
@@ -8588,12 +9447,14 @@
"keywords": [
"country",
"nation",
- "aw"
- ]
+ "aw",
+ "flag"
+ ],
+ "moji": "🇦🇼"
},
"flag_ax": {
"unicode": "1F1E6-1F1FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "åland islands",
"shortname": ":flag_ax:",
"category": "flags",
@@ -8601,7 +9462,11 @@
":ax:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇦🇽"
},
"flag_az": {
"unicode": "1F1E6-1F1FF",
@@ -8617,8 +9482,10 @@
"country",
"nation",
"azarbaycan",
- "az"
- ]
+ "az",
+ "flag"
+ ],
+ "moji": "🇦🇿"
},
"flag_ba": {
"unicode": "1F1E7-1F1E6",
@@ -8634,8 +9501,10 @@
"country",
"nation",
"bosna i hercegovina",
- "ba"
- ]
+ "ba",
+ "flag"
+ ],
+ "moji": "🇧🇦"
},
"flag_bb": {
"unicode": "1F1E7-1F1E7",
@@ -8650,8 +9519,10 @@
"keywords": [
"country",
"nation",
- "bb"
- ]
+ "bb",
+ "flag"
+ ],
+ "moji": "🇧🇧"
},
"flag_bd": {
"unicode": "1F1E7-1F1E9",
@@ -8666,8 +9537,10 @@
"keywords": [
"country",
"nation",
- "bd"
- ]
+ "bd",
+ "flag"
+ ],
+ "moji": "🇧🇩"
},
"flag_be": {
"unicode": "1F1E7-1F1EA",
@@ -8684,8 +9557,10 @@
"nation",
"belgique",
"belgie",
- "be"
- ]
+ "be",
+ "flag"
+ ],
+ "moji": "🇧🇪"
},
"flag_bf": {
"unicode": "1F1E7-1F1EB",
@@ -8700,8 +9575,10 @@
"keywords": [
"country",
"nation",
- "bf"
- ]
+ "bf",
+ "flag"
+ ],
+ "moji": "🇧🇫"
},
"flag_bg": {
"unicode": "1F1E7-1F1EC",
@@ -8716,8 +9593,10 @@
"keywords": [
"country",
"nation",
- "bg"
- ]
+ "bg",
+ "flag"
+ ],
+ "moji": "🇧🇬"
},
"flag_bh": {
"unicode": "1F1E7-1F1ED",
@@ -8733,8 +9612,10 @@
"country",
"nation",
"al bahrayn",
- "bh"
- ]
+ "bh",
+ "flag"
+ ],
+ "moji": "🇧🇭"
},
"flag_bi": {
"unicode": "1F1E7-1F1EE",
@@ -8749,8 +9630,10 @@
"keywords": [
"country",
"nation",
- "bi"
- ]
+ "bi",
+ "flag"
+ ],
+ "moji": "🇧🇮"
},
"flag_bj": {
"unicode": "1F1E7-1F1EF",
@@ -8765,12 +9648,14 @@
"keywords": [
"country",
"nation",
- "bj"
- ]
+ "bj",
+ "flag"
+ ],
+ "moji": "🇧🇯"
},
"flag_bl": {
"unicode": "1F1E7-1F1F1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "saint barthélemy",
"shortname": ":flag_bl:",
"category": "flags",
@@ -8778,22 +9663,28 @@
":bl:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇧🇱"
},
"flag_black": {
"unicode": "1F3F4",
"unicode_alternates": [],
"name": "waving black flag",
"shortname": ":flag_black:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":waving_black_flag:"
],
"aliases_ascii": [],
"keywords": [
"symbol",
- "signal"
- ]
+ "signal",
+ "object"
+ ],
+ "moji": "🏴"
},
"flag_bm": {
"unicode": "1F1E7-1F1F2",
@@ -8808,8 +9699,10 @@
"keywords": [
"country",
"nation",
- "bm"
- ]
+ "bm",
+ "flag"
+ ],
+ "moji": "🇧🇲"
},
"flag_bn": {
"unicode": "1F1E7-1F1F3",
@@ -8824,8 +9717,10 @@
"keywords": [
"country",
"nation",
- "bn"
- ]
+ "bn",
+ "flag"
+ ],
+ "moji": "🇧🇳"
},
"flag_bo": {
"unicode": "1F1E7-1F1F4",
@@ -8840,12 +9735,14 @@
"keywords": [
"country",
"nation",
- "bo"
- ]
+ "bo",
+ "flag"
+ ],
+ "moji": "🇧🇴"
},
"flag_bq": {
"unicode": "1F1E7-1F1F6",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "caribbean netherlands",
"shortname": ":flag_bq:",
"category": "flags",
@@ -8853,7 +9750,11 @@
":bq:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇧🇶"
},
"flag_br": {
"unicode": "1F1E7-1F1F7",
@@ -8869,8 +9770,10 @@
"country",
"nation",
"brasil",
- "br"
- ]
+ "br",
+ "flag"
+ ],
+ "moji": "🇧🇷"
},
"flag_bs": {
"unicode": "1F1E7-1F1F8",
@@ -8885,8 +9788,10 @@
"keywords": [
"country",
"nation",
- "bs"
- ]
+ "bs",
+ "flag"
+ ],
+ "moji": "🇧🇸"
},
"flag_bt": {
"unicode": "1F1E7-1F1F9",
@@ -8901,12 +9806,14 @@
"keywords": [
"country",
"nation",
- "bt"
- ]
+ "bt",
+ "flag"
+ ],
+ "moji": "🇧🇹"
},
"flag_bv": {
"unicode": "1F1E7-1F1FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "bouvet island",
"shortname": ":flag_bv:",
"category": "flags",
@@ -8914,7 +9821,11 @@
":bv:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇧🇻"
},
"flag_bw": {
"unicode": "1F1E7-1F1FC",
@@ -8929,8 +9840,10 @@
"keywords": [
"country",
"nation",
- "bw"
- ]
+ "bw",
+ "flag"
+ ],
+ "moji": "🇧🇼"
},
"flag_by": {
"unicode": "1F1E7-1F1FE",
@@ -8946,8 +9859,10 @@
"country",
"nation",
"byelarus",
- "by"
- ]
+ "by",
+ "flag"
+ ],
+ "moji": "🇧🇾"
},
"flag_bz": {
"unicode": "1F1E7-1F1FF",
@@ -8962,8 +9877,10 @@
"keywords": [
"country",
"nation",
- "bz"
- ]
+ "bz",
+ "flag"
+ ],
+ "moji": "🇧🇿"
},
"flag_ca": {
"unicode": "1F1E8-1F1E6",
@@ -8978,12 +9895,14 @@
"keywords": [
"country",
"nation",
- "ca"
- ]
+ "ca",
+ "flag"
+ ],
+ "moji": "🇨🇦"
},
"flag_cc": {
"unicode": "1F1E8-1F1E8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "cocos (keeling) islands",
"shortname": ":flag_cc:",
"category": "flags",
@@ -8991,7 +9910,11 @@
":cc:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇨🇨"
},
"flag_cd": {
"unicode": "1F1E8-1F1E9",
@@ -9008,8 +9931,10 @@
"nation",
"r&eacute;publique d&eacute;mocratique du congo",
"republique democratique du congo",
- "cd"
- ]
+ "cd",
+ "flag"
+ ],
+ "moji": "🇨🇩"
},
"flag_cf": {
"unicode": "1F1E8-1F1EB",
@@ -9024,8 +9949,10 @@
"keywords": [
"country",
"nation",
- "cf"
- ]
+ "cf",
+ "flag"
+ ],
+ "moji": "🇨🇫"
},
"flag_cg": {
"unicode": "1F1E8-1F1EC",
@@ -9040,8 +9967,10 @@
"keywords": [
"country",
"nation",
- "cg"
- ]
+ "cg",
+ "flag"
+ ],
+ "moji": "🇨🇬"
},
"flag_ch": {
"unicode": "1F1E8-1F1ED",
@@ -9056,8 +9985,11 @@
"keywords": [
"country",
"nation",
- "swiss"
- ]
+ "swiss",
+ "neutral",
+ "flag"
+ ],
+ "moji": "🇨🇭"
},
"flag_ci": {
"unicode": "1F1E8-1F1EE",
@@ -9072,12 +10004,14 @@
"keywords": [
"country",
"nation",
- "ci"
- ]
+ "ci",
+ "flag"
+ ],
+ "moji": "🇨🇮"
},
"flag_ck": {
"unicode": "1F1E8-1F1F0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "cook islands",
"shortname": ":flag_ck:",
"category": "flags",
@@ -9085,7 +10019,11 @@
":ck:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇨🇰"
},
"flag_cl": {
"unicode": "1F1E8-1F1F1",
@@ -9100,8 +10038,10 @@
"keywords": [
"country",
"nation",
- "cl"
- ]
+ "cl",
+ "flag"
+ ],
+ "moji": "🇨🇱"
},
"flag_cm": {
"unicode": "1F1E8-1F1F2",
@@ -9116,8 +10056,10 @@
"keywords": [
"country",
"nation",
- "cm"
- ]
+ "cm",
+ "flag"
+ ],
+ "moji": "🇨🇲"
},
"flag_cn": {
"unicode": "1F1E8-1F1F3",
@@ -9135,8 +10077,10 @@
"zhong guo",
"country",
"nation",
- "cn"
- ]
+ "cn",
+ "flag"
+ ],
+ "moji": "🇨🇳"
},
"flag_co": {
"unicode": "1F1E8-1F1F4",
@@ -9151,12 +10095,14 @@
"keywords": [
"country",
"nation",
- "co"
- ]
+ "co",
+ "flag"
+ ],
+ "moji": "🇨🇴"
},
"flag_cp": {
"unicode": "1F1E8-1F1F5",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "clipperton island",
"shortname": ":flag_cp:",
"category": "flags",
@@ -9164,7 +10110,11 @@
":cp:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇨🇵"
},
"flag_cr": {
"unicode": "1F1E8-1F1F7",
@@ -9179,8 +10129,10 @@
"keywords": [
"country",
"nation",
- "cr"
- ]
+ "cr",
+ "flag"
+ ],
+ "moji": "🇨🇷"
},
"flag_cu": {
"unicode": "1F1E8-1F1FA",
@@ -9195,8 +10147,10 @@
"keywords": [
"country",
"nation",
- "cu"
- ]
+ "cu",
+ "flag"
+ ],
+ "moji": "🇨🇺"
},
"flag_cv": {
"unicode": "1F1E8-1F1FB",
@@ -9212,12 +10166,14 @@
"country",
"nation",
"cabo verde",
- "cv"
- ]
+ "cv",
+ "flag"
+ ],
+ "moji": "🇨🇻"
},
"flag_cw": {
"unicode": "1F1E8-1F1FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "curaçao",
"shortname": ":flag_cw:",
"category": "flags",
@@ -9225,11 +10181,15 @@
":cw:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇨🇼"
},
"flag_cx": {
"unicode": "1F1E8-1F1FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "christmas island",
"shortname": ":flag_cx:",
"category": "flags",
@@ -9237,7 +10197,11 @@
":cx:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇨🇽"
},
"flag_cy": {
"unicode": "1F1E8-1F1FE",
@@ -9254,8 +10218,10 @@
"nation",
"kibris",
"kypros",
- "cy"
- ]
+ "cy",
+ "flag"
+ ],
+ "moji": "🇨🇾"
},
"flag_cz": {
"unicode": "1F1E8-1F1FF",
@@ -9271,8 +10237,10 @@
"country",
"nation",
"ceska republika",
- "cz"
- ]
+ "cz",
+ "flag"
+ ],
+ "moji": "🇨🇿"
},
"flag_de": {
"unicode": "1F1E9-1F1EA",
@@ -9289,12 +10257,14 @@
"nation",
"deutschland",
"country",
- "de"
- ]
+ "de",
+ "flag"
+ ],
+ "moji": "🇩🇪"
},
"flag_dg": {
"unicode": "1F1E9-1F1EC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "diego garcia",
"shortname": ":flag_dg:",
"category": "flags",
@@ -9302,7 +10272,11 @@
":dg:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇩🇬"
},
"flag_dj": {
"unicode": "1F1E9-1F1EF",
@@ -9317,8 +10291,10 @@
"keywords": [
"country",
"nation",
- "dj"
- ]
+ "dj",
+ "flag"
+ ],
+ "moji": "🇩🇯"
},
"flag_dk": {
"unicode": "1F1E9-1F1F0",
@@ -9334,8 +10310,10 @@
"country",
"nation",
"danmark",
- "dk"
- ]
+ "dk",
+ "flag"
+ ],
+ "moji": "🇩🇰"
},
"flag_dm": {
"unicode": "1F1E9-1F1F2",
@@ -9350,8 +10328,10 @@
"keywords": [
"country",
"nation",
- "dm"
- ]
+ "dm",
+ "flag"
+ ],
+ "moji": "🇩🇲"
},
"flag_do": {
"unicode": "1F1E9-1F1F4",
@@ -9366,8 +10346,10 @@
"keywords": [
"country",
"nation",
- "do"
- ]
+ "do",
+ "flag"
+ ],
+ "moji": "🇩🇴"
},
"flag_dz": {
"unicode": "1F1E9-1F1FF",
@@ -9384,12 +10366,14 @@
"nation",
"al jaza'ir",
"al jazair",
- "dz"
- ]
+ "dz",
+ "flag"
+ ],
+ "moji": "🇩🇿"
},
"flag_ea": {
"unicode": "1F1EA-1F1E6",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ceuta, melilla",
"shortname": ":flag_ea:",
"category": "flags",
@@ -9397,7 +10381,11 @@
":ea:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇪🇦"
},
"flag_ec": {
"unicode": "1F1EA-1F1E8",
@@ -9412,8 +10400,10 @@
"keywords": [
"country",
"nation",
- "ec"
- ]
+ "ec",
+ "flag"
+ ],
+ "moji": "🇪🇨"
},
"flag_ee": {
"unicode": "1F1EA-1F1EA",
@@ -9429,8 +10419,10 @@
"country",
"nation",
"eesti vabariik",
- "ee"
- ]
+ "ee",
+ "flag"
+ ],
+ "moji": "🇪🇪"
},
"flag_eg": {
"unicode": "1F1EA-1F1EC",
@@ -9446,8 +10438,10 @@
"country",
"nation",
"misr",
- "eg"
- ]
+ "eg",
+ "flag"
+ ],
+ "moji": "🇪🇬"
},
"flag_eh": {
"unicode": "1F1EA-1F1ED",
@@ -9465,8 +10459,10 @@
"aṣ-Ṣaḥrā’ al-gharbīyah",
"sahra",
"gharbiyah",
- "eh"
- ]
+ "eh",
+ "flag"
+ ],
+ "moji": "🇪🇭"
},
"flag_er": {
"unicode": "1F1EA-1F1F7",
@@ -9482,8 +10478,10 @@
"country",
"nation",
"hagere ertra",
- "er"
- ]
+ "er",
+ "flag"
+ ],
+ "moji": "🇪🇷"
},
"flag_es": {
"unicode": "1F1EA-1F1F8",
@@ -9500,8 +10498,10 @@
"espa&ntilde;a",
"country",
"espana",
- "es"
- ]
+ "es",
+ "flag"
+ ],
+ "moji": "🇪🇸"
},
"flag_et": {
"unicode": "1F1EA-1F1F9",
@@ -9518,12 +10518,14 @@
"nation",
"ityop'iya",
"ityopiya",
- "et"
- ]
+ "et",
+ "flag"
+ ],
+ "moji": "🇪🇹"
},
"flag_eu": {
"unicode": "1F1EA-1F1FA",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "european union",
"shortname": ":flag_eu:",
"category": "flags",
@@ -9531,7 +10533,11 @@
":eu:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇪🇺"
},
"flag_fi": {
"unicode": "1F1EB-1F1EE",
@@ -9547,8 +10553,10 @@
"country",
"nation",
"suomen tasavalta",
- "fi"
- ]
+ "fi",
+ "flag"
+ ],
+ "moji": "🇫🇮"
},
"flag_fj": {
"unicode": "1F1EB-1F1EF",
@@ -9563,8 +10571,10 @@
"keywords": [
"country",
"nation",
- "fj"
- ]
+ "fj",
+ "flag"
+ ],
+ "moji": "🇫🇯"
},
"flag_fk": {
"unicode": "1F1EB-1F1F0",
@@ -9580,8 +10590,10 @@
"country",
"nation",
"islas malvinas",
- "fk"
- ]
+ "fk",
+ "flag"
+ ],
+ "moji": "🇫🇰"
},
"flag_fm": {
"unicode": "1F1EB-1F1F2",
@@ -9596,8 +10608,10 @@
"keywords": [
"country",
"nation",
- "fm"
- ]
+ "fm",
+ "flag"
+ ],
+ "moji": "🇫🇲"
},
"flag_fo": {
"unicode": "1F1EB-1F1F4",
@@ -9613,8 +10627,10 @@
"country",
"nation",
"foroyar",
- "fo"
- ]
+ "fo",
+ "flag"
+ ],
+ "moji": "🇫🇴"
},
"flag_fr": {
"unicode": "1F1EB-1F1F7",
@@ -9630,8 +10646,10 @@
"french",
"nation",
"country",
- "fr"
- ]
+ "fr",
+ "flag"
+ ],
+ "moji": "🇫🇷"
},
"flag_ga": {
"unicode": "1F1EC-1F1E6",
@@ -9646,8 +10664,10 @@
"keywords": [
"country",
"nation",
- "ga"
- ]
+ "ga",
+ "flag"
+ ],
+ "moji": "🇬🇦"
},
"flag_gb": {
"unicode": "1F1EC-1F1E7",
@@ -9666,8 +10686,10 @@
"nation",
"united kingdom",
"england",
- "country"
- ]
+ "country",
+ "flag"
+ ],
+ "moji": "🇬🇧"
},
"flag_gd": {
"unicode": "1F1EC-1F1E9",
@@ -9682,8 +10704,10 @@
"keywords": [
"country",
"nation",
- "gd"
- ]
+ "gd",
+ "flag"
+ ],
+ "moji": "🇬🇩"
},
"flag_ge": {
"unicode": "1F1EC-1F1EA",
@@ -9700,12 +10724,14 @@
"nation",
"sak'art'velo",
"sakartvelo",
- "ge"
- ]
+ "ge",
+ "flag"
+ ],
+ "moji": "🇬🇪"
},
"flag_gf": {
"unicode": "1F1EC-1F1EB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "french guiana",
"shortname": ":flag_gf:",
"category": "flags",
@@ -9713,11 +10739,15 @@
":gf:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇬🇫"
},
"flag_gg": {
"unicode": "1F1EC-1F1EC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guernsey",
"shortname": ":flag_gg:",
"category": "flags",
@@ -9725,7 +10755,11 @@
":gg:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇬🇬"
},
"flag_gh": {
"unicode": "1F1EC-1F1ED",
@@ -9740,8 +10774,10 @@
"keywords": [
"country",
"nation",
- "gh"
- ]
+ "gh",
+ "flag"
+ ],
+ "moji": "🇬🇭"
},
"flag_gi": {
"unicode": "1F1EC-1F1EE",
@@ -9756,8 +10792,10 @@
"keywords": [
"country",
"nation",
- "gi"
- ]
+ "gi",
+ "flag"
+ ],
+ "moji": "🇬🇮"
},
"flag_gl": {
"unicode": "1F1EC-1F1F1",
@@ -9773,8 +10811,10 @@
"country",
"nation",
"kalaallit nunaat",
- "gl"
- ]
+ "gl",
+ "flag"
+ ],
+ "moji": "🇬🇱"
},
"flag_gm": {
"unicode": "1F1EC-1F1F2",
@@ -9789,8 +10829,10 @@
"keywords": [
"country",
"nation",
- "gm"
- ]
+ "gm",
+ "flag"
+ ],
+ "moji": "🇬🇲"
},
"flag_gn": {
"unicode": "1F1EC-1F1F3",
@@ -9806,12 +10848,14 @@
"country",
"nation",
"guinee",
- "gn"
- ]
+ "gn",
+ "flag"
+ ],
+ "moji": "🇬🇳"
},
"flag_gp": {
"unicode": "1F1EC-1F1F5",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guadeloupe",
"shortname": ":flag_gp:",
"category": "flags",
@@ -9819,7 +10863,11 @@
":gp:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇬🇵"
},
"flag_gq": {
"unicode": "1F1EC-1F1F6",
@@ -9835,8 +10883,10 @@
"country",
"nation",
"guinea ecuatorial",
- "gq"
- ]
+ "gq",
+ "flag"
+ ],
+ "moji": "🇬🇶"
},
"flag_gr": {
"unicode": "1F1EC-1F1F7",
@@ -9853,12 +10903,14 @@
"nation",
"ellas",
"ellada",
- "gr"
- ]
+ "gr",
+ "flag"
+ ],
+ "moji": "🇬🇷"
},
"flag_gs": {
"unicode": "1F1EC-1F1F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "south georgia",
"shortname": ":flag_gs:",
"category": "flags",
@@ -9866,7 +10918,11 @@
":gs:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇬🇸"
},
"flag_gt": {
"unicode": "1F1EC-1F1F9",
@@ -9881,8 +10937,10 @@
"keywords": [
"country",
"nation",
- "gt"
- ]
+ "gt",
+ "flag"
+ ],
+ "moji": "🇬🇹"
},
"flag_gu": {
"unicode": "1F1EC-1F1FA",
@@ -9897,8 +10955,10 @@
"keywords": [
"country",
"nation",
- "gu"
- ]
+ "gu",
+ "flag"
+ ],
+ "moji": "🇬🇺"
},
"flag_gw": {
"unicode": "1F1EC-1F1FC",
@@ -9915,8 +10975,10 @@
"nation",
"guine-bissau",
"guine bissau",
- "gw"
- ]
+ "gw",
+ "flag"
+ ],
+ "moji": "🇬🇼"
},
"flag_gy": {
"unicode": "1F1EC-1F1FE",
@@ -9931,8 +10993,10 @@
"keywords": [
"country",
"nation",
- "gy"
- ]
+ "gy",
+ "flag"
+ ],
+ "moji": "🇬🇾"
},
"flag_hk": {
"unicode": "1F1ED-1F1F0",
@@ -9948,12 +11012,14 @@
"country",
"nation",
"xianggang",
- "hk"
- ]
+ "hk",
+ "flag"
+ ],
+ "moji": "🇭🇰"
},
"flag_hm": {
"unicode": "1F1ED-1F1F2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "heard island and mcdonald islands",
"shortname": ":flag_hm:",
"category": "flags",
@@ -9961,7 +11027,11 @@
":hm:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇭🇲"
},
"flag_hn": {
"unicode": "1F1ED-1F1F3",
@@ -9976,8 +11046,10 @@
"keywords": [
"country",
"nation",
- "hn"
- ]
+ "hn",
+ "flag"
+ ],
+ "moji": "🇭🇳"
},
"flag_hr": {
"unicode": "1F1ED-1F1F7",
@@ -9993,8 +11065,10 @@
"country",
"nation",
"hrvatska",
- "hr"
- ]
+ "hr",
+ "flag"
+ ],
+ "moji": "🇭🇷"
},
"flag_ht": {
"unicode": "1F1ED-1F1F9",
@@ -10009,8 +11083,10 @@
"keywords": [
"country",
"nation",
- "ht"
- ]
+ "ht",
+ "flag"
+ ],
+ "moji": "🇭🇹"
},
"flag_hu": {
"unicode": "1F1ED-1F1FA",
@@ -10026,12 +11102,14 @@
"country",
"nation",
"magyarorszag",
- "hu"
- ]
+ "hu",
+ "flag"
+ ],
+ "moji": "🇭🇺"
},
"flag_ic": {
"unicode": "1F1EE-1F1E8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "canary islands",
"shortname": ":flag_ic:",
"category": "flags",
@@ -10039,7 +11117,11 @@
":ic:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇮🇨"
},
"flag_id": {
"unicode": "1F1EE-1F1E9",
@@ -10054,8 +11136,10 @@
"keywords": [
"country",
"nation",
- "id"
- ]
+ "id",
+ "flag"
+ ],
+ "moji": "🇮🇩"
},
"flag_ie": {
"unicode": "1F1EE-1F1EA",
@@ -10072,8 +11156,10 @@
"nation",
"&eacute;ire",
"eire",
- "ie"
- ]
+ "ie",
+ "flag"
+ ],
+ "moji": "🇮🇪"
},
"flag_il": {
"unicode": "1F1EE-1F1F1",
@@ -10090,12 +11176,15 @@
"nation",
"yisra'el",
"yisrael",
- "il"
- ]
+ "il",
+ "jew",
+ "flag"
+ ],
+ "moji": "🇮🇱"
},
"flag_im": {
"unicode": "1F1EE-1F1F2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "isle of man",
"shortname": ":flag_im:",
"category": "flags",
@@ -10103,7 +11192,11 @@
":im:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇮🇲"
},
"flag_in": {
"unicode": "1F1EE-1F1F3",
@@ -10119,12 +11212,14 @@
"country",
"nation",
"bharat",
- "in"
- ]
+ "in",
+ "flag"
+ ],
+ "moji": "🇮🇳"
},
"flag_io": {
"unicode": "1F1EE-1F1F4",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "british indian ocean territory",
"shortname": ":flag_io:",
"category": "flags",
@@ -10132,7 +11227,11 @@
":io:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇮🇴"
},
"flag_iq": {
"unicode": "1F1EE-1F1F6",
@@ -10147,8 +11246,10 @@
"keywords": [
"country",
"nation",
- "iq"
- ]
+ "iq",
+ "flag"
+ ],
+ "moji": "🇮🇶"
},
"flag_ir": {
"unicode": "1F1EE-1F1F7",
@@ -10163,8 +11264,10 @@
"keywords": [
"country",
"nation",
- "ir"
- ]
+ "ir",
+ "flag"
+ ],
+ "moji": "🇮🇷"
},
"flag_is": {
"unicode": "1F1EE-1F1F8",
@@ -10180,8 +11283,10 @@
"country",
"nation",
"lyoveldio island",
- "is"
- ]
+ "is",
+ "flag"
+ ],
+ "moji": "🇮🇸"
},
"flag_it": {
"unicode": "1F1EE-1F1F9",
@@ -10197,8 +11302,11 @@
"italia",
"country",
"nation",
- "it"
- ]
+ "it",
+ "italian",
+ "flag"
+ ],
+ "moji": "🇮🇹"
},
"flag_je": {
"unicode": "1F1EF-1F1EA",
@@ -10213,8 +11321,10 @@
"keywords": [
"country",
"nation",
- "je"
- ]
+ "je",
+ "flag"
+ ],
+ "moji": "🇯🇪"
},
"flag_jm": {
"unicode": "1F1EF-1F1F2",
@@ -10229,8 +11339,10 @@
"keywords": [
"country",
"nation",
- "jm"
- ]
+ "jm",
+ "flag"
+ ],
+ "moji": "🇯🇲"
},
"flag_jo": {
"unicode": "1F1EF-1F1F4",
@@ -10246,8 +11358,10 @@
"country",
"nation",
"al urdun",
- "jo"
- ]
+ "jo",
+ "flag"
+ ],
+ "moji": "🇯🇴"
},
"flag_jp": {
"unicode": "1F1EF-1F1F5",
@@ -10263,8 +11377,11 @@
"nation",
"nippon",
"country",
- "jp"
- ]
+ "jp",
+ "japan",
+ "flag"
+ ],
+ "moji": "🇯🇵"
},
"flag_ke": {
"unicode": "1F1F0-1F1EA",
@@ -10279,8 +11396,10 @@
"keywords": [
"country",
"nation",
- "ke"
- ]
+ "ke",
+ "flag"
+ ],
+ "moji": "🇰🇪"
},
"flag_kg": {
"unicode": "1F1F0-1F1EC",
@@ -10296,8 +11415,10 @@
"country",
"nation",
"kyrgyz respublikasy",
- "kg"
- ]
+ "kg",
+ "flag"
+ ],
+ "moji": "🇰🇬"
},
"flag_kh": {
"unicode": "1F1F0-1F1ED",
@@ -10313,8 +11434,10 @@
"country",
"nation",
"kampuchea",
- "kh"
- ]
+ "kh",
+ "flag"
+ ],
+ "moji": "🇰🇭"
},
"flag_ki": {
"unicode": "1F1F0-1F1EE",
@@ -10331,8 +11454,10 @@
"nation",
"kiribati",
"kiribas",
- "ki"
- ]
+ "ki",
+ "flag"
+ ],
+ "moji": "🇰🇮"
},
"flag_km": {
"unicode": "1F1F0-1F1F2",
@@ -10347,8 +11472,10 @@
"keywords": [
"country",
"nation",
- "km"
- ]
+ "km",
+ "flag"
+ ],
+ "moji": "🇰🇲"
},
"flag_kn": {
"unicode": "1F1F0-1F1F3",
@@ -10363,8 +11490,10 @@
"keywords": [
"country",
"nation",
- "kn"
- ]
+ "kn",
+ "flag"
+ ],
+ "moji": "🇰🇳"
},
"flag_kp": {
"unicode": "1F1F0-1F1F5",
@@ -10379,8 +11508,10 @@
"keywords": [
"country",
"nation",
- "kp"
- ]
+ "kp",
+ "flag"
+ ],
+ "moji": "🇰🇵"
},
"flag_kr": {
"unicode": "1F1F0-1F1F7",
@@ -10396,8 +11527,10 @@
"nation",
"country",
"south korea",
- "kr"
- ]
+ "kr",
+ "flag"
+ ],
+ "moji": "🇰🇷"
},
"flag_kw": {
"unicode": "1F1F0-1F1FC",
@@ -10413,8 +11546,10 @@
"country",
"nation",
"al kuwayt",
- "kw"
- ]
+ "kw",
+ "flag"
+ ],
+ "moji": "🇰🇼"
},
"flag_ky": {
"unicode": "1F1F0-1F1FE",
@@ -10429,8 +11564,10 @@
"keywords": [
"country",
"nation",
- "ky"
- ]
+ "ky",
+ "flag"
+ ],
+ "moji": "🇰🇾"
},
"flag_kz": {
"unicode": "1F1F0-1F1FF",
@@ -10446,8 +11583,10 @@
"country",
"nation",
"qazaqstan",
- "kz"
- ]
+ "kz",
+ "flag"
+ ],
+ "moji": "🇰🇿"
},
"flag_la": {
"unicode": "1F1F1-1F1E6",
@@ -10462,8 +11601,10 @@
"keywords": [
"country",
"nation",
- "la"
- ]
+ "la",
+ "flag"
+ ],
+ "moji": "🇱🇦"
},
"flag_lb": {
"unicode": "1F1F1-1F1E7",
@@ -10479,8 +11620,10 @@
"country",
"nation",
"lubnan",
- "lb"
- ]
+ "lb",
+ "flag"
+ ],
+ "moji": "🇱🇧"
},
"flag_lc": {
"unicode": "1F1F1-1F1E8",
@@ -10495,8 +11638,10 @@
"keywords": [
"country",
"nation",
- "lc"
- ]
+ "lc",
+ "flag"
+ ],
+ "moji": "🇱🇨"
},
"flag_li": {
"unicode": "1F1F1-1F1EE",
@@ -10511,8 +11656,10 @@
"keywords": [
"country",
"nation",
- "li"
- ]
+ "li",
+ "flag"
+ ],
+ "moji": "🇱🇮"
},
"flag_lk": {
"unicode": "1F1F1-1F1F0",
@@ -10527,8 +11674,10 @@
"keywords": [
"country",
"nation",
- "lk"
- ]
+ "lk",
+ "flag"
+ ],
+ "moji": "🇱🇰"
},
"flag_lr": {
"unicode": "1F1F1-1F1F7",
@@ -10543,8 +11692,10 @@
"keywords": [
"country",
"nation",
- "lr"
- ]
+ "lr",
+ "flag"
+ ],
+ "moji": "🇱🇷"
},
"flag_ls": {
"unicode": "1F1F1-1F1F8",
@@ -10559,8 +11710,10 @@
"keywords": [
"country",
"nation",
- "ls"
- ]
+ "ls",
+ "flag"
+ ],
+ "moji": "🇱🇸"
},
"flag_lt": {
"unicode": "1F1F1-1F1F9",
@@ -10576,8 +11729,10 @@
"country",
"nation",
"lietuva",
- "lt"
- ]
+ "lt",
+ "flag"
+ ],
+ "moji": "🇱🇹"
},
"flag_lu": {
"unicode": "1F1F1-1F1FA",
@@ -10594,8 +11749,10 @@
"nation",
"luxembourg",
"letzebuerg",
- "lu"
- ]
+ "lu",
+ "flag"
+ ],
+ "moji": "🇱🇺"
},
"flag_lv": {
"unicode": "1F1F1-1F1FB",
@@ -10611,8 +11768,10 @@
"country",
"nation",
"latvija",
- "lv"
- ]
+ "lv",
+ "flag"
+ ],
+ "moji": "🇱🇻"
},
"flag_ly": {
"unicode": "1F1F1-1F1FE",
@@ -10628,8 +11787,10 @@
"country",
"nation",
"libiyah",
- "ly"
- ]
+ "ly",
+ "flag"
+ ],
+ "moji": "🇱🇾"
},
"flag_ma": {
"unicode": "1F1F2-1F1E6",
@@ -10645,8 +11806,10 @@
"country",
"nation",
"al maghrib",
- "ma"
- ]
+ "ma",
+ "flag"
+ ],
+ "moji": "🇲🇦"
},
"flag_mc": {
"unicode": "1F1F2-1F1E8",
@@ -10661,8 +11824,10 @@
"keywords": [
"country",
"nation",
- "mc"
- ]
+ "mc",
+ "flag"
+ ],
+ "moji": "🇲🇨"
},
"flag_md": {
"unicode": "1F1F2-1F1E9",
@@ -10677,8 +11842,10 @@
"keywords": [
"country",
"nation",
- "md"
- ]
+ "md",
+ "flag"
+ ],
+ "moji": "🇲🇩"
},
"flag_me": {
"unicode": "1F1F2-1F1EA",
@@ -10694,12 +11861,14 @@
"country",
"nation",
"crna gora",
- "me"
- ]
+ "me",
+ "flag"
+ ],
+ "moji": "🇲🇪"
},
"flag_mf": {
"unicode": "1F1F2-1F1EB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "saint martin",
"shortname": ":flag_mf:",
"category": "flags",
@@ -10707,7 +11876,11 @@
":mf:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇲🇫"
},
"flag_mg": {
"unicode": "1F1F2-1F1EC",
@@ -10722,8 +11895,10 @@
"keywords": [
"country",
"nation",
- "mg"
- ]
+ "mg",
+ "flag"
+ ],
+ "moji": "🇲🇬"
},
"flag_mh": {
"unicode": "1F1F2-1F1ED",
@@ -10738,8 +11913,10 @@
"keywords": [
"country",
"nation",
- "mh"
- ]
+ "mh",
+ "flag"
+ ],
+ "moji": "🇲🇭"
},
"flag_mk": {
"unicode": "1F1F2-1F1F0",
@@ -10754,8 +11931,10 @@
"keywords": [
"country",
"nation",
- "mk"
- ]
+ "mk",
+ "flag"
+ ],
+ "moji": "🇲🇰"
},
"flag_ml": {
"unicode": "1F1F2-1F1F1",
@@ -10770,8 +11949,10 @@
"keywords": [
"country",
"nation",
- "ml"
- ]
+ "ml",
+ "flag"
+ ],
+ "moji": "🇲🇱"
},
"flag_mm": {
"unicode": "1F1F2-1F1F2",
@@ -10787,8 +11968,10 @@
"country",
"nation",
"myanma naingngandaw",
- "mm"
- ]
+ "mm",
+ "flag"
+ ],
+ "moji": "🇲🇲"
},
"flag_mn": {
"unicode": "1F1F2-1F1F3",
@@ -10804,8 +11987,10 @@
"country",
"nation",
"mongol uls",
- "mn"
- ]
+ "mn",
+ "flag"
+ ],
+ "moji": "🇲🇳"
},
"flag_mo": {
"unicode": "1F1F2-1F1F4",
@@ -10821,12 +12006,14 @@
"country",
"nation",
"aomen",
- "mo"
- ]
+ "mo",
+ "flag"
+ ],
+ "moji": "🇲🇴"
},
"flag_mp": {
"unicode": "1F1F2-1F1F5",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "northern mariana islands",
"shortname": ":flag_mp:",
"category": "flags",
@@ -10834,11 +12021,15 @@
":mp:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇲🇵"
},
"flag_mq": {
"unicode": "1F1F2-1F1F6",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "martinique",
"shortname": ":flag_mq:",
"category": "flags",
@@ -10846,7 +12037,11 @@
":mq:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇲🇶"
},
"flag_mr": {
"unicode": "1F1F2-1F1F7",
@@ -10862,8 +12057,10 @@
"country",
"nation",
"muritaniyah",
- "mr"
- ]
+ "mr",
+ "flag"
+ ],
+ "moji": "🇲🇷"
},
"flag_ms": {
"unicode": "1F1F2-1F1F8",
@@ -10878,8 +12075,10 @@
"keywords": [
"country",
"nation",
- "ms"
- ]
+ "ms",
+ "flag"
+ ],
+ "moji": "🇲🇸"
},
"flag_mt": {
"unicode": "1F1F2-1F1F9",
@@ -10894,8 +12093,10 @@
"keywords": [
"country",
"nation",
- "mt"
- ]
+ "mt",
+ "flag"
+ ],
+ "moji": "🇲🇹"
},
"flag_mu": {
"unicode": "1F1F2-1F1FA",
@@ -10910,8 +12111,10 @@
"keywords": [
"country",
"nation",
- "mu"
- ]
+ "mu",
+ "flag"
+ ],
+ "moji": "🇲🇺"
},
"flag_mv": {
"unicode": "1F1F2-1F1FB",
@@ -10927,8 +12130,10 @@
"country",
"nation",
"dhivehi raajje",
- "mv"
- ]
+ "mv",
+ "flag"
+ ],
+ "moji": "🇲🇻"
},
"flag_mw": {
"unicode": "1F1F2-1F1FC",
@@ -10943,8 +12148,10 @@
"keywords": [
"country",
"nation",
- "mw"
- ]
+ "mw",
+ "flag"
+ ],
+ "moji": "🇲🇼"
},
"flag_mx": {
"unicode": "1F1F2-1F1FD",
@@ -10959,8 +12166,11 @@
"keywords": [
"country",
"nation",
- "mx"
- ]
+ "mx",
+ "mexican",
+ "flag"
+ ],
+ "moji": "🇲🇽"
},
"flag_my": {
"unicode": "1F1F2-1F1FE",
@@ -10975,8 +12185,10 @@
"keywords": [
"country",
"nation",
- "my"
- ]
+ "my",
+ "flag"
+ ],
+ "moji": "🇲🇾"
},
"flag_mz": {
"unicode": "1F1F2-1F1FF",
@@ -10992,8 +12204,10 @@
"country",
"nation",
"mocambique",
- "mz"
- ]
+ "mz",
+ "flag"
+ ],
+ "moji": "🇲🇿"
},
"flag_na": {
"unicode": "1F1F3-1F1E6",
@@ -11008,8 +12222,10 @@
"keywords": [
"country",
"nation",
- "na"
- ]
+ "na",
+ "flag"
+ ],
+ "moji": "🇳🇦"
},
"flag_nc": {
"unicode": "1F1F3-1F1E8",
@@ -11027,8 +12243,10 @@
"nouvelle",
"cal&eacute;donie",
"caledonie",
- "nc"
- ]
+ "nc",
+ "flag"
+ ],
+ "moji": "🇳🇨"
},
"flag_ne": {
"unicode": "1F1F3-1F1EA",
@@ -11043,12 +12261,14 @@
"keywords": [
"country",
"nation",
- "ne"
- ]
+ "ne",
+ "flag"
+ ],
+ "moji": "🇳🇪"
},
"flag_nf": {
"unicode": "1F1F3-1F1EB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "norfolk island",
"shortname": ":flag_nf:",
"category": "flags",
@@ -11056,7 +12276,11 @@
":nf:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇳🇫"
},
"flag_ng": {
"unicode": "1F1F3-1F1EC",
@@ -11071,8 +12295,10 @@
"keywords": [
"country",
"nation",
- "ng"
- ]
+ "ng",
+ "flag"
+ ],
+ "moji": "🇳🇬"
},
"flag_ni": {
"unicode": "1F1F3-1F1EE",
@@ -11087,8 +12313,10 @@
"keywords": [
"country",
"nation",
- "ni"
- ]
+ "ni",
+ "flag"
+ ],
+ "moji": "🇳🇮"
},
"flag_nl": {
"unicode": "1F1F3-1F1F1",
@@ -11105,8 +12333,10 @@
"nation",
"nederland",
"holland",
- "nl"
- ]
+ "nl",
+ "flag"
+ ],
+ "moji": "🇳🇱"
},
"flag_no": {
"unicode": "1F1F3-1F1F4",
@@ -11122,8 +12352,10 @@
"country",
"nation",
"norge",
- "no"
- ]
+ "no",
+ "flag"
+ ],
+ "moji": "🇳🇴"
},
"flag_np": {
"unicode": "1F1F3-1F1F5",
@@ -11138,8 +12370,10 @@
"keywords": [
"country",
"nation",
- "np"
- ]
+ "np",
+ "flag"
+ ],
+ "moji": "🇳🇵"
},
"flag_nr": {
"unicode": "1F1F3-1F1F7",
@@ -11154,8 +12388,10 @@
"keywords": [
"country",
"nation",
- "nr"
- ]
+ "nr",
+ "flag"
+ ],
+ "moji": "🇳🇷"
},
"flag_nu": {
"unicode": "1F1F3-1F1FA",
@@ -11170,8 +12406,10 @@
"keywords": [
"country",
"nation",
- "nu"
- ]
+ "nu",
+ "flag"
+ ],
+ "moji": "🇳🇺"
},
"flag_nz": {
"unicode": "1F1F3-1F1FF",
@@ -11187,8 +12425,10 @@
"country",
"nation",
"aotearoa",
- "nz"
- ]
+ "nz",
+ "flag"
+ ],
+ "moji": "🇳🇿"
},
"flag_om": {
"unicode": "1F1F4-1F1F2",
@@ -11204,8 +12444,10 @@
"country",
"nation",
"saltanat uman",
- "om"
- ]
+ "om",
+ "flag"
+ ],
+ "moji": "🇴🇲"
},
"flag_pa": {
"unicode": "1F1F5-1F1E6",
@@ -11220,8 +12462,10 @@
"keywords": [
"country",
"nation",
- "pa"
- ]
+ "pa",
+ "flag"
+ ],
+ "moji": "🇵🇦"
},
"flag_pe": {
"unicode": "1F1F5-1F1EA",
@@ -11236,8 +12480,10 @@
"keywords": [
"country",
"nation",
- "pe"
- ]
+ "pe",
+ "flag"
+ ],
+ "moji": "🇵🇪"
},
"flag_pf": {
"unicode": "1F1F5-1F1EB",
@@ -11254,8 +12500,10 @@
"nation",
"polyn&eacute;sie fran&ccedil;aise",
"polynesie francaise",
- "pf"
- ]
+ "pf",
+ "flag"
+ ],
+ "moji": "🇵🇫"
},
"flag_pg": {
"unicode": "1F1F5-1F1EC",
@@ -11271,8 +12519,10 @@
"country",
"nation",
"papua niu gini",
- "pg"
- ]
+ "pg",
+ "flag"
+ ],
+ "moji": "🇵🇬"
},
"flag_ph": {
"unicode": "1F1F5-1F1ED",
@@ -11288,8 +12538,10 @@
"country",
"nation",
"pilipinas",
- "ph"
- ]
+ "ph",
+ "flag"
+ ],
+ "moji": "🇵🇭"
},
"flag_pk": {
"unicode": "1F1F5-1F1F0",
@@ -11304,8 +12556,10 @@
"keywords": [
"country",
"nation",
- "pk"
- ]
+ "pk",
+ "flag"
+ ],
+ "moji": "🇵🇰"
},
"flag_pl": {
"unicode": "1F1F5-1F1F1",
@@ -11321,12 +12575,14 @@
"country",
"nation",
"polska",
- "pl"
- ]
+ "pl",
+ "flag"
+ ],
+ "moji": "🇵🇱"
},
"flag_pm": {
"unicode": "1F1F5-1F1F2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "saint pierre and miquelon",
"shortname": ":flag_pm:",
"category": "flags",
@@ -11334,11 +12590,15 @@
":pm:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇵🇲"
},
"flag_pn": {
"unicode": "1F1F5-1F1F3",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pitcairn",
"shortname": ":flag_pn:",
"category": "flags",
@@ -11346,7 +12606,11 @@
":pn:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇵🇳"
},
"flag_pr": {
"unicode": "1F1F5-1F1F7",
@@ -11361,8 +12625,10 @@
"keywords": [
"country",
"nation",
- "pr"
- ]
+ "pr",
+ "flag"
+ ],
+ "moji": "🇵🇷"
},
"flag_ps": {
"unicode": "1F1F5-1F1F8",
@@ -11377,8 +12643,10 @@
"keywords": [
"country",
"nation",
- "ps"
- ]
+ "ps",
+ "flag"
+ ],
+ "moji": "🇵🇸"
},
"flag_pt": {
"unicode": "1F1F5-1F1F9",
@@ -11393,8 +12661,10 @@
"keywords": [
"country",
"nation",
- "pt"
- ]
+ "pt",
+ "flag"
+ ],
+ "moji": "🇵🇹"
},
"flag_pw": {
"unicode": "1F1F5-1F1FC",
@@ -11410,8 +12680,10 @@
"country",
"nation",
"belau",
- "pw"
- ]
+ "pw",
+ "flag"
+ ],
+ "moji": "🇵🇼"
},
"flag_py": {
"unicode": "1F1F5-1F1FE",
@@ -11426,8 +12698,10 @@
"keywords": [
"country",
"nation",
- "py"
- ]
+ "py",
+ "flag"
+ ],
+ "moji": "🇵🇾"
},
"flag_qa": {
"unicode": "1F1F6-1F1E6",
@@ -11443,12 +12717,14 @@
"country",
"nation",
"dawlat qatar",
- "qa"
- ]
+ "qa",
+ "flag"
+ ],
+ "moji": "🇶🇦"
},
"flag_re": {
"unicode": "1F1F7-1F1EA",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "réunion",
"shortname": ":flag_re:",
"category": "flags",
@@ -11456,7 +12732,11 @@
":re:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇷🇪"
},
"flag_ro": {
"unicode": "1F1F7-1F1F4",
@@ -11471,8 +12751,10 @@
"keywords": [
"country",
"nation",
- "ro"
- ]
+ "ro",
+ "flag"
+ ],
+ "moji": "🇷🇴"
},
"flag_rs": {
"unicode": "1F1F7-1F1F8",
@@ -11488,8 +12770,10 @@
"country",
"nation",
"srbija",
- "rs"
- ]
+ "rs",
+ "flag"
+ ],
+ "moji": "🇷🇸"
},
"flag_ru": {
"unicode": "1F1F7-1F1FA",
@@ -11505,8 +12789,10 @@
"nation",
"russian",
"country",
- "ru"
- ]
+ "ru",
+ "flag"
+ ],
+ "moji": "🇷🇺"
},
"flag_rw": {
"unicode": "1F1F7-1F1FC",
@@ -11521,8 +12807,10 @@
"keywords": [
"country",
"nation",
- "rw"
- ]
+ "rw",
+ "flag"
+ ],
+ "moji": "🇷🇼"
},
"flag_sa": {
"unicode": "1F1F8-1F1E6",
@@ -11539,8 +12827,10 @@
"country",
"nation",
"al arabiyah as suudiyah",
- "sa"
- ]
+ "sa",
+ "flag"
+ ],
+ "moji": "🇸🇦"
},
"flag_sb": {
"unicode": "1F1F8-1F1E7",
@@ -11555,8 +12845,10 @@
"keywords": [
"country",
"nation",
- "sb"
- ]
+ "sb",
+ "flag"
+ ],
+ "moji": "🇸🇧"
},
"flag_sc": {
"unicode": "1F1F8-1F1E8",
@@ -11572,8 +12864,10 @@
"country",
"nation",
"seychelles",
- "sc"
- ]
+ "sc",
+ "flag"
+ ],
+ "moji": "🇸🇨"
},
"flag_sd": {
"unicode": "1F1F8-1F1E9",
@@ -11589,8 +12883,10 @@
"country",
"nation",
"as-sudan",
- "sd"
- ]
+ "sd",
+ "flag"
+ ],
+ "moji": "🇸🇩"
},
"flag_se": {
"unicode": "1F1F8-1F1EA",
@@ -11606,8 +12902,10 @@
"country",
"nation",
"sverige",
- "se"
- ]
+ "se",
+ "flag"
+ ],
+ "moji": "🇸🇪"
},
"flag_sg": {
"unicode": "1F1F8-1F1EC",
@@ -11622,8 +12920,10 @@
"keywords": [
"country",
"nation",
- "sg"
- ]
+ "sg",
+ "flag"
+ ],
+ "moji": "🇸🇬"
},
"flag_sh": {
"unicode": "1F1F8-1F1ED",
@@ -11638,8 +12938,10 @@
"keywords": [
"country",
"nation",
- "sh"
- ]
+ "sh",
+ "flag"
+ ],
+ "moji": "🇸🇭"
},
"flag_si": {
"unicode": "1F1F8-1F1EE",
@@ -11655,12 +12957,14 @@
"country",
"nation",
"slovenija",
- "si"
- ]
+ "si",
+ "flag"
+ ],
+ "moji": "🇸🇮"
},
"flag_sj": {
"unicode": "1F1F8-1F1EF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "svalbard and jan mayen",
"shortname": ":flag_sj:",
"category": "flags",
@@ -11668,7 +12972,11 @@
":sj:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇸🇯"
},
"flag_sk": {
"unicode": "1F1F8-1F1F0",
@@ -11683,8 +12991,10 @@
"keywords": [
"country",
"nation",
- "sk"
- ]
+ "sk",
+ "flag"
+ ],
+ "moji": "🇸🇰"
},
"flag_sl": {
"unicode": "1F1F8-1F1F1",
@@ -11699,8 +13009,10 @@
"keywords": [
"country",
"nation",
- "sl"
- ]
+ "sl",
+ "flag"
+ ],
+ "moji": "🇸🇱"
},
"flag_sm": {
"unicode": "1F1F8-1F1F2",
@@ -11715,8 +13027,10 @@
"keywords": [
"country",
"nation",
- "sm"
- ]
+ "sm",
+ "flag"
+ ],
+ "moji": "🇸🇲"
},
"flag_sn": {
"unicode": "1F1F8-1F1F3",
@@ -11731,8 +13045,10 @@
"keywords": [
"country",
"nation",
- "sn"
- ]
+ "sn",
+ "flag"
+ ],
+ "moji": "🇸🇳"
},
"flag_so": {
"unicode": "1F1F8-1F1F4",
@@ -11747,8 +13063,10 @@
"keywords": [
"country",
"nation",
- "so"
- ]
+ "so",
+ "flag"
+ ],
+ "moji": "🇸🇴"
},
"flag_sr": {
"unicode": "1F1F8-1F1F7",
@@ -11763,12 +13081,14 @@
"keywords": [
"country",
"nation",
- "sr"
- ]
+ "sr",
+ "flag"
+ ],
+ "moji": "🇸🇷"
},
"flag_ss": {
"unicode": "1F1F8-1F1F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "south sudan",
"shortname": ":flag_ss:",
"category": "flags",
@@ -11776,7 +13096,11 @@
":ss:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇸🇸"
},
"flag_st": {
"unicode": "1F1F8-1F1F9",
@@ -11792,8 +13116,10 @@
"country",
"nation",
"sao tome e principe",
- "st"
- ]
+ "st",
+ "flag"
+ ],
+ "moji": "🇸🇹"
},
"flag_sv": {
"unicode": "1F1F8-1F1FB",
@@ -11808,12 +13134,14 @@
"keywords": [
"country",
"nation",
- "sv"
- ]
+ "sv",
+ "flag"
+ ],
+ "moji": "🇸🇻"
},
"flag_sx": {
"unicode": "1F1F8-1F1FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sint maarten",
"shortname": ":flag_sx:",
"category": "flags",
@@ -11821,7 +13149,11 @@
":sx:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇸🇽"
},
"flag_sy": {
"unicode": "1F1F8-1F1FE",
@@ -11836,8 +13168,10 @@
"keywords": [
"country",
"nation",
- "sy"
- ]
+ "sy",
+ "flag"
+ ],
+ "moji": "🇸🇾"
},
"flag_sz": {
"unicode": "1F1F8-1F1FF",
@@ -11852,12 +13186,14 @@
"keywords": [
"country",
"nation",
- "sz"
- ]
+ "sz",
+ "flag"
+ ],
+ "moji": "🇸🇿"
},
"flag_ta": {
"unicode": "1F1F9-1F1E6",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "tristan da cunha",
"shortname": ":flag_ta:",
"category": "flags",
@@ -11865,11 +13201,15 @@
":ta:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇹🇦"
},
"flag_tc": {
"unicode": "1F1F9-1F1E8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "turks and caicos islands",
"shortname": ":flag_tc:",
"category": "flags",
@@ -11877,7 +13217,11 @@
":tc:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇹🇨"
},
"flag_td": {
"unicode": "1F1F9-1F1E9",
@@ -11893,12 +13237,14 @@
"country",
"nation",
"tchad",
- "td"
- ]
+ "td",
+ "flag"
+ ],
+ "moji": "🇹🇩"
},
"flag_tf": {
"unicode": "1F1F9-1F1EB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "french southern territories",
"shortname": ":flag_tf:",
"category": "flags",
@@ -11906,7 +13252,11 @@
":tf:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇹🇫"
},
"flag_tg": {
"unicode": "1F1F9-1F1EC",
@@ -11922,8 +13272,10 @@
"country",
"nation",
"republique togolaise",
- "tg"
- ]
+ "tg",
+ "flag"
+ ],
+ "moji": "🇹🇬"
},
"flag_th": {
"unicode": "1F1F9-1F1ED",
@@ -11939,8 +13291,10 @@
"country",
"nation",
"prathet thai",
- "th"
- ]
+ "th",
+ "flag"
+ ],
+ "moji": "🇹🇭"
},
"flag_tj": {
"unicode": "1F1F9-1F1EF",
@@ -11956,12 +13310,14 @@
"country",
"nation",
"jumhurii tojikiston",
- "tj"
- ]
+ "tj",
+ "flag"
+ ],
+ "moji": "🇹🇯"
},
"flag_tk": {
"unicode": "1F1F9-1F1F0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "tokelau",
"shortname": ":flag_tk:",
"category": "flags",
@@ -11969,7 +13325,11 @@
":tk:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇹🇰"
},
"flag_tl": {
"unicode": "1F1F9-1F1F1",
@@ -11984,8 +13344,10 @@
"keywords": [
"country",
"nation",
- "tl"
- ]
+ "tl",
+ "flag"
+ ],
+ "moji": "🇹🇱"
},
"flag_tm": {
"unicode": "1F1F9-1F1F2",
@@ -12000,8 +13362,10 @@
"keywords": [
"country",
"nation",
- "tm"
- ]
+ "tm",
+ "flag"
+ ],
+ "moji": "🇹🇲"
},
"flag_tn": {
"unicode": "1F1F9-1F1F3",
@@ -12017,8 +13381,10 @@
"country",
"nation",
"tunis",
- "tn"
- ]
+ "tn",
+ "flag"
+ ],
+ "moji": "🇹🇳"
},
"flag_to": {
"unicode": "1F1F9-1F1F4",
@@ -12033,8 +13399,10 @@
"keywords": [
"country",
"nation",
- "to"
- ]
+ "to",
+ "flag"
+ ],
+ "moji": "🇹🇴"
},
"flag_tr": {
"unicode": "1F1F9-1F1F7",
@@ -12049,8 +13417,10 @@
"keywords": [
"country",
"nation",
- "turkiye"
- ]
+ "turkiye",
+ "flag"
+ ],
+ "moji": "🇹🇷"
},
"flag_tt": {
"unicode": "1F1F9-1F1F9",
@@ -12065,8 +13435,10 @@
"keywords": [
"country",
"nation",
- "tt"
- ]
+ "tt",
+ "flag"
+ ],
+ "moji": "🇹🇹"
},
"flag_tv": {
"unicode": "1F1F9-1F1FB",
@@ -12081,8 +13453,10 @@
"keywords": [
"country",
"nation",
- "tv"
- ]
+ "tv",
+ "flag"
+ ],
+ "moji": "🇹🇻"
},
"flag_tw": {
"unicode": "1F1F9-1F1FC",
@@ -12098,8 +13472,10 @@
"country",
"nation",
"taiwan",
- "tw"
- ]
+ "tw",
+ "flag"
+ ],
+ "moji": "🇹🇼"
},
"flag_tz": {
"unicode": "1F1F9-1F1FF",
@@ -12114,8 +13490,10 @@
"keywords": [
"country",
"nation",
- "tz"
- ]
+ "tz",
+ "flag"
+ ],
+ "moji": "🇹🇿"
},
"flag_ua": {
"unicode": "1F1FA-1F1E6",
@@ -12131,8 +13509,10 @@
"country",
"nation",
"ukrayina",
- "ua"
- ]
+ "ua",
+ "flag"
+ ],
+ "moji": "🇺🇦"
},
"flag_ug": {
"unicode": "1F1FA-1F1EC",
@@ -12147,12 +13527,14 @@
"keywords": [
"country",
"nation",
- "ug"
- ]
+ "ug",
+ "flag"
+ ],
+ "moji": "🇺🇬"
},
"flag_um": {
"unicode": "1F1FA-1F1F2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "united states minor outlying islands",
"shortname": ":flag_um:",
"category": "flags",
@@ -12160,7 +13542,11 @@
":um:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇺🇲"
},
"flag_us": {
"unicode": "1F1FA-1F1F8",
@@ -12180,8 +13566,10 @@
"united states of america",
"america",
"old glory",
- "us"
- ]
+ "us",
+ "flag"
+ ],
+ "moji": "🇺🇸"
},
"flag_uy": {
"unicode": "1F1FA-1F1FE",
@@ -12196,8 +13584,10 @@
"keywords": [
"country",
"nation",
- "uy"
- ]
+ "uy",
+ "flag"
+ ],
+ "moji": "🇺🇾"
},
"flag_uz": {
"unicode": "1F1FA-1F1FF",
@@ -12213,8 +13603,10 @@
"country",
"nation",
"uzbekiston respublikasi",
- "uz"
- ]
+ "uz",
+ "flag"
+ ],
+ "moji": "🇺🇿"
},
"flag_va": {
"unicode": "1F1FB-1F1E6",
@@ -12229,8 +13621,10 @@
"keywords": [
"country",
"nation",
- "va"
- ]
+ "va",
+ "flag"
+ ],
+ "moji": "🇻🇦"
},
"flag_vc": {
"unicode": "1F1FB-1F1E8",
@@ -12245,8 +13639,10 @@
"keywords": [
"country",
"nation",
- "vc"
- ]
+ "vc",
+ "flag"
+ ],
+ "moji": "🇻🇨"
},
"flag_ve": {
"unicode": "1F1FB-1F1EA",
@@ -12261,12 +13657,14 @@
"keywords": [
"country",
"nation",
- "ve"
- ]
+ "ve",
+ "flag"
+ ],
+ "moji": "🇻🇪"
},
"flag_vg": {
"unicode": "1F1FB-1F1EC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "british virgin islands",
"shortname": ":flag_vg:",
"category": "flags",
@@ -12274,7 +13672,11 @@
":vg:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇻🇬"
},
"flag_vi": {
"unicode": "1F1FB-1F1EE",
@@ -12289,8 +13691,10 @@
"keywords": [
"country",
"nation",
- "vi"
- ]
+ "vi",
+ "flag"
+ ],
+ "moji": "🇻🇮"
},
"flag_vn": {
"unicode": "1F1FB-1F1F3",
@@ -12306,8 +13710,10 @@
"country",
"nation",
"viet nam",
- "vn"
- ]
+ "vn",
+ "flag"
+ ],
+ "moji": "🇻🇳"
},
"flag_vu": {
"unicode": "1F1FB-1F1FA",
@@ -12322,8 +13728,10 @@
"keywords": [
"country",
"nation",
- "vu"
- ]
+ "vu",
+ "flag"
+ ],
+ "moji": "🇻🇺"
},
"flag_wf": {
"unicode": "1F1FC-1F1EB",
@@ -12338,23 +13746,27 @@
"keywords": [
"country",
"nation",
- "wf"
- ]
+ "wf",
+ "flag"
+ ],
+ "moji": "🇼🇫"
},
"flag_white": {
"unicode": "1F3F3",
"unicode_alternates": [],
"name": "waving white flag",
"shortname": ":flag_white:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":waving_white_flag:"
],
"aliases_ascii": [],
"keywords": [
"symbol",
- "signal"
- ]
+ "signal",
+ "object"
+ ],
+ "moji": "🏳"
},
"flag_ws": {
"unicode": "1F1FC-1F1F8",
@@ -12370,8 +13782,10 @@
"country",
"nation",
"american samoa",
- "ws"
- ]
+ "ws",
+ "flag"
+ ],
+ "moji": "🇼🇸"
},
"flag_xk": {
"unicode": "1F1FD-1F1F0",
@@ -12386,8 +13800,10 @@
"keywords": [
"country",
"nation",
- "xk"
- ]
+ "xk",
+ "flag"
+ ],
+ "moji": "🇽🇰"
},
"flag_ye": {
"unicode": "1F1FE-1F1EA",
@@ -12403,12 +13819,14 @@
"country",
"nation",
"al yaman",
- "ye"
- ]
+ "ye",
+ "flag"
+ ],
+ "moji": "🇾🇪"
},
"flag_yt": {
"unicode": "1F1FE-1F1F9",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mayotte",
"shortname": ":flag_yt:",
"category": "flags",
@@ -12416,7 +13834,11 @@
":yt:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "country",
+ "flag"
+ ],
+ "moji": "🇾🇹"
},
"flag_za": {
"unicode": "1F1FF-1F1E6",
@@ -12430,8 +13852,10 @@
"aliases_ascii": [],
"keywords": [
"country",
- "nation"
- ]
+ "nation",
+ "flag"
+ ],
+ "moji": "🇿🇦"
},
"flag_zm": {
"unicode": "1F1FF-1F1F2",
@@ -12446,8 +13870,10 @@
"keywords": [
"country",
"nation",
- "zm"
- ]
+ "zm",
+ "flag"
+ ],
+ "moji": "🇿🇲"
},
"flag_zw": {
"unicode": "1F1FF-1F1FC",
@@ -12462,8 +13888,10 @@
"keywords": [
"country",
"nation",
- "zw"
- ]
+ "zw",
+ "flag"
+ ],
+ "moji": "🇿🇼"
},
"flags": {
"unicode": "1F38F",
@@ -12484,11 +13912,11 @@
"boys",
"celebration",
"happiness",
- "carp",
"streamers",
- "japanese",
"holiday",
- "flags"
+ "flags",
+ "object",
+ "japan"
],
"moji": "🎏"
},
@@ -12501,56 +13929,25 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "dark"
+ "dark",
+ "electronics",
+ "object"
],
"moji": "🔦"
},
"fleur-de-lis": {
"unicode": "269C",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fleur-de-lis",
"shortname": ":fleur-de-lis:",
"category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "symbol"
- ]
- },
- "flip_phone": {
- "unicode": "1F581",
- "unicode_alternates": [],
- "name": "clamshell mobile phone",
- "shortname": ":flip_phone:",
- "category": "objects_symbols",
- "aliases": [
- ":clamshell_mobile_phone:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "cellphone"
- ]
- },
- "floppy_black": {
- "unicode": "1F5AA",
- "unicode_alternates": [],
- "name": "black hard shell floppy disk",
- "shortname": ":floppy_black:",
- "category": "objects_symbols",
- "aliases": [
- ":black_hard_shell_floppy_disk:"
+ "symbol",
+ "object"
],
- "aliases_ascii": [],
- "keywords": [
- "oldschool",
- "save",
- "technology",
- "storage",
- "information",
- "computer",
- "drive",
- "megabyte"
- ]
+ "moji": "⚜"
},
"floppy_disk": {
"unicode": "1F4BE",
@@ -12570,37 +13967,18 @@
"information",
"computer",
"drive",
- "megabyte"
+ "megabyte",
+ "electronics",
+ "office"
],
"moji": "💾"
},
- "floppy_white": {
- "unicode": "1F5AB",
- "unicode_alternates": [],
- "name": "white hard shell floppy disk",
- "shortname": ":floppy_white:",
- "category": "objects_symbols",
- "aliases": [
- ":white_hard_shell_floppy_disk:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "oldschool",
- "save",
- "technology",
- "storage",
- "information",
- "computer",
- "drive",
- "megabyte"
- ]
- },
"flower_playing_cards": {
"unicode": "1F3B4",
"unicode_alternates": [],
"name": "flower playing cards",
"shortname": ":flower_playing_cards:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12610,7 +13988,9 @@
"game",
"august",
"moon",
- "special"
+ "special",
+ "object",
+ "symbol"
],
"moji": "🎴"
},
@@ -12619,7 +13999,7 @@
"unicode_alternates": [],
"name": "flushed face",
"shortname": ":flushed:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":$",
@@ -12630,11 +14010,13 @@
"face",
"flattered",
"flush",
- "blush",
"red",
"pink",
"cheeks",
- "shy"
+ "shy",
+ "smiley",
+ "emotion",
+ "omg"
],
"moji": "😳"
},
@@ -12650,15 +14032,18 @@
"weather",
"damp",
"cloud",
- "hazy"
- ]
+ "hazy",
+ "sky",
+ "cold"
+ ],
+ "moji": "🌫"
},
"foggy": {
"unicode": "1F301",
"unicode_alternates": [],
"name": "foggy",
"shortname": ":foggy:",
- "category": "nature",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12667,43 +14052,21 @@
"bridge",
"weather",
"fog",
- "foggy"
+ "foggy",
+ "places",
+ "building",
+ "sky",
+ "travel",
+ "vacation"
],
"moji": "🌁"
},
- "folder": {
- "unicode": "1F5C0",
- "unicode_alternates": [],
- "name": "folder",
- "shortname": ":folder:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "documents"
- ]
- },
- "folder_open": {
- "unicode": "1F5C1",
- "unicode_alternates": [],
- "name": "open folder",
- "shortname": ":folder_open:",
- "category": "objects_symbols",
- "aliases": [
- ":open_folder:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "documents",
- "load"
- ]
- },
"football": {
"unicode": "1F3C8",
"unicode_alternates": [],
"name": "american football",
"shortname": ":football:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12714,7 +14077,8 @@
"ball",
"sport",
"america",
- "american"
+ "american",
+ "game"
],
"moji": "🏈"
},
@@ -12723,7 +14087,7 @@
"unicode_alternates": [],
"name": "footprints",
"shortname": ":footprints:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12736,7 +14100,7 @@
"unicode_alternates": [],
"name": "fork and knife",
"shortname": ":fork_and_knife:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12747,7 +14111,9 @@
"restaurant",
"meal",
"food",
- "eat"
+ "eat",
+ "object",
+ "weapon"
],
"moji": "🍴"
},
@@ -12756,7 +14122,7 @@
"unicode_alternates": [],
"name": "fork and knife with plate",
"shortname": ":fork_knife_plate:",
- "category": "travel_places",
+ "category": "food",
"aliases": [
":fork_and_knife_with_plate:"
],
@@ -12768,8 +14134,10 @@
"lunch",
"dinner",
"utensils",
- "setting"
- ]
+ "setting",
+ "object"
+ ],
+ "moji": "🍽"
},
"fountain": {
"unicode": "26F2",
@@ -12778,11 +14146,13 @@
],
"name": "fountain",
"shortname": ":fountain:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "photo"
+ "photo",
+ "travel",
+ "vacation"
],
"moji": "⛲"
},
@@ -12792,15 +14162,18 @@
"unicode_alternates": [
"0034-FE0F-20E3"
],
- "name": "digit four",
+ "name": "keycap digit four",
"shortname": ":four:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"4",
"blue-square",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"four_leaf_clover": {
@@ -12823,74 +14196,75 @@
"irish",
"saint",
"patrick",
- "green"
+ "green",
+ "sol"
],
"moji": "🍀"
},
- "frame_photo": {
- "unicode": "1F5BC",
+ "fox": {
+ "unicode": "1F98A",
"unicode_alternates": [],
- "name": "frame with picture",
- "shortname": ":frame_photo:",
- "category": "objects_symbols",
+ "name": "fox face",
+ "shortname": ":fox:",
+ "category": "nature",
"aliases": [
- ":frame_with_picture:"
+ ":fox_face:"
],
"aliases_ascii": [],
- "keywords": [
- "photo"
- ]
+ "keywords": [],
+ "moji": "🦊"
},
- "frame_tiles": {
- "unicode": "1F5BD",
+ "frame_photo": {
+ "unicode": "1F5BC",
"unicode_alternates": [],
- "name": "frame with tiles",
- "shortname": ":frame_tiles:",
- "category": "objects_symbols",
+ "name": "frame with picture",
+ "shortname": ":frame_photo:",
+ "category": "objects",
"aliases": [
- ":frame_with_tiles:"
+ ":frame_with_picture:"
],
"aliases_ascii": [],
"keywords": [
"photo",
- "painting"
- ]
- },
- "frame_x": {
- "unicode": "1F5BE",
- "unicode_alternates": [],
- "name": "frame with an x",
- "shortname": ":frame_x:",
- "category": "objects_symbols",
- "aliases": [
- ":frame_with_an_x:"
+ "travel",
+ "vacation"
],
- "aliases_ascii": [],
- "keywords": [
- "photo",
- "painting"
- ]
+ "moji": "🖼"
},
"free": {
"unicode": "1F193",
"unicode_alternates": [],
"name": "squared free",
"shortname": ":free:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "words"
+ "words",
+ "symbol"
],
"moji": "🆓"
},
+ "french_bread": {
+ "unicode": "1F956",
+ "unicode_alternates": [],
+ "name": "baguette bread",
+ "shortname": ":french_bread:",
+ "category": "food",
+ "aliases": [
+ ":baguette_bread:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥖"
+ },
"fried_shrimp": {
"unicode": "1F364",
"unicode_alternates": [],
"name": "fried shrimp",
"shortname": ":fried_shrimp:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12909,7 +14283,7 @@
"unicode_alternates": [],
"name": "french fries",
"shortname": ":fries:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -12920,7 +14294,8 @@
"potato",
"fry",
"russet",
- "idaho"
+ "idaho",
+ "america"
],
"moji": "🍟"
},
@@ -12934,7 +14309,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐸"
},
@@ -12943,7 +14319,7 @@
"unicode_alternates": [],
"name": "frowning face with open mouth",
"shortname": ":frowning:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":anguished:"
],
@@ -12955,13 +14331,16 @@
"sad",
"pout",
"sulk",
- "glower"
+ "glower",
+ "smiley",
+ "surprised",
+ "emotion"
],
"moji": "😦"
},
"frowning2": {
"unicode": "2639",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white frowning face",
"shortname": ":frowning2:",
"category": "people",
@@ -12971,8 +14350,12 @@
"aliases_ascii": [],
"keywords": [
"frown",
- "person"
- ]
+ "person",
+ "sad",
+ "smiley",
+ "emotion"
+ ],
+ "moji": "☹"
},
"fuelpump": {
"unicode": "26FD",
@@ -12981,12 +14364,14 @@
],
"name": "fuel pump",
"shortname": ":fuelpump:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"gas station",
- "petroleum"
+ "petroleum",
+ "object",
+ "gas pump"
],
"moji": "⛽"
},
@@ -13010,7 +14395,8 @@
"monster",
"spooky",
"werewolves",
- "twilight"
+ "twilight",
+ "space"
],
"moji": "🌕"
},
@@ -13029,12 +14415,13 @@
"anthropomorphic",
"face",
"sky",
- "night",
"cheese",
"phase",
"spooky",
"werewolves",
- "monsters"
+ "monsters",
+ "space",
+ "goodnight"
],
"moji": "🌝"
},
@@ -13043,23 +14430,24 @@
"unicode_alternates": [],
"name": "game die",
"shortname": ":game_die:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"dice",
"game",
"die",
- "dice",
"craps",
"gamble",
- "play"
+ "play",
+ "object",
+ "boys night"
],
"moji": "🎲"
},
"gear": {
"unicode": "2699",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "gear",
"shortname": ":gear:",
"category": "objects",
@@ -13068,19 +14456,22 @@
"keywords": [
"object",
"tool"
- ]
+ ],
+ "moji": "⚙"
},
"gem": {
"unicode": "1F48E",
"unicode_alternates": [],
"name": "gem stone",
"shortname": ":gem:",
- "category": "emoticons",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue",
- "ruby"
+ "ruby",
+ "object",
+ "gem"
],
"moji": "💎"
},
@@ -13091,7 +14482,7 @@
],
"name": "gemini",
"shortname": ":gemini:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13103,9 +14494,8 @@
"stars",
"zodiac",
"sign",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♊"
},
@@ -13114,11 +14504,13 @@
"unicode_alternates": [],
"name": "ghost",
"shortname": ":ghost:",
- "category": "objects",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "halloween"
+ "halloween",
+ "holidays",
+ "monster"
],
"moji": "👻"
},
@@ -13136,11 +14528,12 @@
"present",
"xmas",
"gift",
- "present",
"wrap",
"package",
- "birthday",
- "wedding"
+ "wedding",
+ "object",
+ "holidays",
+ "parties"
],
"moji": "🎁"
},
@@ -13149,12 +14542,14 @@
"unicode_alternates": [],
"name": "heart with ribbon",
"shortname": ":gift_heart:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"love",
- "valentines"
+ "valentines",
+ "symbol",
+ "condolence"
],
"moji": "💝"
},
@@ -13163,18 +14558,22 @@
"unicode_alternates": [],
"name": "girl",
"shortname": ":girl:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"female",
- "woman"
+ "woman",
+ "people",
+ "women",
+ "baby",
+ "diversity"
],
"moji": "👧"
},
"girl_tone1": {
"unicode": "1F467-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "girl tone 1",
"shortname": ":girl_tone1:",
"category": "people",
@@ -13184,11 +14583,12 @@
"female",
"kid",
"child"
- ]
+ ],
+ "moji": "👧🏻"
},
"girl_tone2": {
"unicode": "1F467-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "girl tone 2",
"shortname": ":girl_tone2:",
"category": "people",
@@ -13198,11 +14598,12 @@
"female",
"kid",
"child"
- ]
+ ],
+ "moji": "👧🏼"
},
"girl_tone3": {
"unicode": "1F467-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "girl tone 3",
"shortname": ":girl_tone3:",
"category": "people",
@@ -13212,11 +14613,12 @@
"female",
"kid",
"child"
- ]
+ ],
+ "moji": "👧🏽"
},
"girl_tone4": {
"unicode": "1F467-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "girl tone 4",
"shortname": ":girl_tone4:",
"category": "people",
@@ -13226,11 +14628,12 @@
"female",
"kid",
"child"
- ]
+ ],
+ "moji": "👧🏾"
},
"girl_tone5": {
"unicode": "1F467-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "girl tone 5",
"shortname": ":girl_tone5:",
"category": "people",
@@ -13240,42 +14643,43 @@
"female",
"kid",
"child"
- ]
- },
- "girls_symbol": {
- "unicode": "1F6CA",
- "unicode_alternates": [],
- "name": "girls symbol",
- "shortname": ":girls_symbol:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "female",
- "child"
- ]
+ ],
+ "moji": "👧🏿"
},
"globe_with_meridians": {
"unicode": "1F310",
"unicode_alternates": [],
"name": "globe with meridians",
"shortname": ":globe_with_meridians:",
- "category": "nature",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"earth",
"international",
"world",
- "earth",
"meridian",
"globe",
"space",
"planet",
- "home"
+ "home",
+ "symbol"
],
"moji": "🌐"
},
+ "goal": {
+ "unicode": "1F945",
+ "unicode_alternates": [],
+ "name": "goal net",
+ "shortname": ":goal:",
+ "category": "activity",
+ "aliases": [
+ ":goal_net:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥅"
+ },
"goat": {
"unicode": "1F410",
"unicode_alternates": [],
@@ -13302,12 +14706,17 @@
],
"name": "flag in hole",
"shortname": ":golf:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"business",
- "sports"
+ "sports",
+ "game",
+ "ball",
+ "vacation",
+ "sport",
+ "golf"
],
"moji": "⛳"
},
@@ -13324,15 +14733,32 @@
"par",
"birdie",
"eagle",
- "mulligan"
- ]
+ "mulligan",
+ "men",
+ "game",
+ "ball",
+ "vacation",
+ "golf"
+ ],
+ "moji": "🏌"
+ },
+ "gorilla": {
+ "unicode": "1F98D",
+ "unicode_alternates": [],
+ "name": "gorilla",
+ "shortname": ":gorilla:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦍"
},
"grapes": {
"unicode": "1F347",
"unicode_alternates": [],
"name": "grapes",
"shortname": ":grapes:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13341,7 +14767,6 @@
"grapes",
"wine",
"vinegar",
- "fruit",
"cluster",
"vine"
],
@@ -13352,19 +14777,19 @@
"unicode_alternates": [],
"name": "green apple",
"shortname": ":green_apple:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fruit",
"nature",
"apple",
- "fruit",
"green",
"pie",
"granny",
"smith",
- "core"
+ "core",
+ "food"
],
"moji": "🍏"
},
@@ -13379,7 +14804,10 @@
"keywords": [
"knowledge",
"library",
- "read"
+ "read",
+ "object",
+ "office",
+ "book"
],
"moji": "📗"
},
@@ -13388,7 +14816,7 @@
"unicode_alternates": [],
"name": "green heart",
"shortname": ":green_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13398,14 +14826,14 @@
"valentines",
"green",
"heart",
- "love",
"nature",
"rebirth",
"reborn",
"jealous",
"clingy",
"envious",
- "possessive"
+ "possessive",
+ "symbol"
],
"moji": "💚"
},
@@ -13414,11 +14842,13 @@
"unicode_alternates": [],
"name": "white exclamation mark ornament",
"shortname": ":grey_exclamation:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "surprise"
+ "surprise",
+ "symbol",
+ "punctuation"
],
"moji": "❕"
},
@@ -13427,11 +14857,13 @@
"unicode_alternates": [],
"name": "white question mark ornament",
"shortname": ":grey_question:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "doubts"
+ "doubts",
+ "symbol",
+ "punctuation"
],
"moji": "❔"
},
@@ -13440,16 +14872,19 @@
"unicode_alternates": [],
"name": "grimacing face",
"shortname": ":grimacing:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"face",
"grimace",
"teeth",
- "grimace",
"disapprove",
- "pain"
+ "pain",
+ "silly",
+ "smiley",
+ "emotion",
+ "selfie"
],
"moji": "😬"
},
@@ -13458,7 +14893,7 @@
"unicode_alternates": [],
"name": "grinning face with smiling eyes",
"shortname": ":grin:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13469,8 +14904,11 @@
"grin",
"grinning",
"smiling",
- "smile",
- "smiley"
+ "smiley",
+ "silly",
+ "emotion",
+ "good",
+ "selfie"
],
"moji": "😁"
},
@@ -13479,7 +14917,7 @@
"unicode_alternates": [],
"name": "grinning face",
"shortname": ":grinning:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13490,17 +14928,17 @@
"grin",
"grinning",
"smiling",
- "smile",
- "smiley"
+ "smiley",
+ "emotion"
],
- "moji": "🕧"
+ "moji": "😀"
},
"guardsman": {
"unicode": "1F482",
"unicode_alternates": [],
"name": "guardsman",
"shortname": ":guardsman:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13513,16 +14951,19 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
- "military"
+ "military",
+ "people",
+ "men",
+ "diversity",
+ "job"
],
"moji": "💂"
},
"guardsman_tone1": {
"unicode": "1F482-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guardsman tone 1",
"shortname": ":guardsman_tone1:",
"category": "people",
@@ -13537,15 +14978,15 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
"military"
- ]
+ ],
+ "moji": "💂🏻"
},
"guardsman_tone2": {
"unicode": "1F482-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guardsman tone 2",
"shortname": ":guardsman_tone2:",
"category": "people",
@@ -13560,15 +15001,15 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
"military"
- ]
+ ],
+ "moji": "💂🏼"
},
"guardsman_tone3": {
"unicode": "1F482-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guardsman tone 3",
"shortname": ":guardsman_tone3:",
"category": "people",
@@ -13583,15 +15024,15 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
"military"
- ]
+ ],
+ "moji": "💂🏽"
},
"guardsman_tone4": {
"unicode": "1F482-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guardsman tone 4",
"shortname": ":guardsman_tone4:",
"category": "people",
@@ -13606,15 +15047,15 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
"military"
- ]
+ ],
+ "moji": "💂🏾"
},
"guardsman_tone5": {
"unicode": "1F482-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "guardsman tone 5",
"shortname": ":guardsman_tone5:",
"category": "people",
@@ -13629,18 +15070,18 @@
"guard",
"bearskin",
"hat",
- "british",
"queen",
"ceremonial",
"military"
- ]
+ ],
+ "moji": "💂🏿"
},
"guitar": {
"unicode": "1F3B8",
"unicode_alternates": [],
"name": "guitar",
"shortname": ":guitar:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13648,12 +15089,11 @@
"music",
"guitar",
"string",
- "music",
- "instrument",
"jam",
"rock",
"acoustic",
- "electric"
+ "electric",
+ "instruments"
],
"moji": "🎸"
},
@@ -13667,7 +15107,11 @@
"aliases_ascii": [],
"keywords": [
"violence",
- "weapon"
+ "weapon",
+ "object",
+ "dead",
+ "gun",
+ "sarcastic"
],
"moji": "🔫"
},
@@ -13676,19 +15120,22 @@
"unicode_alternates": [],
"name": "haircut",
"shortname": ":haircut:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"female",
"girl",
- "woman"
+ "woman",
+ "people",
+ "women",
+ "diversity"
],
"moji": "💇"
},
"haircut_tone1": {
"unicode": "1F487-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "haircut tone 1",
"shortname": ":haircut_tone1:",
"category": "people",
@@ -13698,11 +15145,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💇🏻"
},
"haircut_tone2": {
"unicode": "1F487-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "haircut tone 2",
"shortname": ":haircut_tone2:",
"category": "people",
@@ -13712,11 +15160,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💇🏼"
},
"haircut_tone3": {
"unicode": "1F487-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "haircut tone 3",
"shortname": ":haircut_tone3:",
"category": "people",
@@ -13726,11 +15175,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💇🏽"
},
"haircut_tone4": {
"unicode": "1F487-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "haircut tone 4",
"shortname": ":haircut_tone4:",
"category": "people",
@@ -13740,11 +15190,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💇🏾"
},
"haircut_tone5": {
"unicode": "1F487-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "haircut tone 5",
"shortname": ":haircut_tone5:",
"category": "people",
@@ -13754,14 +15205,15 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💇🏿"
},
"hamburger": {
"unicode": "1F354",
"unicode_alternates": [],
"name": "hamburger",
"shortname": ":hamburger:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -13769,9 +15221,9 @@
"meat",
"hamburger",
"burger",
- "meat",
"cow",
- "beef"
+ "beef",
+ "america"
],
"moji": "🍔"
},
@@ -13789,13 +15241,16 @@
"law",
"ruling",
"tools",
- "verdict"
+ "verdict",
+ "object",
+ "tool",
+ "weapon"
],
"moji": "🔨"
},
"hammer_pick": {
"unicode": "2692",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "hammer and pick",
"shortname": ":hammer_pick:",
"category": "objects",
@@ -13805,8 +15260,10 @@
"aliases_ascii": [],
"keywords": [
"object",
- "tool"
- ]
+ "tool",
+ "weapon"
+ ],
+ "moji": "⚒"
},
"hamster": {
"unicode": "1F439",
@@ -13836,29 +15293,16 @@
"hi",
"five",
"stop",
- "halt"
- ]
- },
- "hand_splayed_reverse": {
- "unicode": "1F591",
- "unicode_alternates": [],
- "name": "reversed raised hand with fingers splayed",
- "shortname": ":hand_splayed_reverse:",
- "category": "people",
- "aliases": [
- ":reversed_raised_hand_with_fingers_splayed:"
+ "halt",
+ "body",
+ "hands",
+ "diversity"
],
- "aliases_ascii": [],
- "keywords": [
- "hi",
- "five",
- "stop",
- "halt"
- ]
+ "moji": "🖐"
},
"hand_splayed_tone1": {
"unicode": "1F590-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with fingers splayed tone 1",
"shortname": ":hand_splayed_tone1:",
"category": "people",
@@ -13871,11 +15315,12 @@
"five",
"stop",
"halt"
- ]
+ ],
+ "moji": "🖐🏻"
},
"hand_splayed_tone2": {
"unicode": "1F590-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with fingers splayed tone 2",
"shortname": ":hand_splayed_tone2:",
"category": "people",
@@ -13888,11 +15333,12 @@
"five",
"stop",
"halt"
- ]
+ ],
+ "moji": "🖐🏼"
},
"hand_splayed_tone3": {
"unicode": "1F590-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with fingers splayed tone 3",
"shortname": ":hand_splayed_tone3:",
"category": "people",
@@ -13905,11 +15351,12 @@
"five",
"stop",
"halt"
- ]
+ ],
+ "moji": "🖐🏽"
},
"hand_splayed_tone4": {
"unicode": "1F590-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with fingers splayed tone 4",
"shortname": ":hand_splayed_tone4:",
"category": "people",
@@ -13922,11 +15369,12 @@
"five",
"stop",
"halt"
- ]
+ ],
+ "moji": "🖐🏾"
},
"hand_splayed_tone5": {
"unicode": "1F590-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with fingers splayed tone 5",
"shortname": ":hand_splayed_tone5:",
"category": "people",
@@ -13939,57 +15387,170 @@
"five",
"stop",
"halt"
- ]
- },
- "hand_victory": {
- "unicode": "1F594",
- "unicode_alternates": [],
- "name": "reversed victory hand",
- "shortname": ":hand_victory:",
- "category": "people",
- "aliases": [
- ":reversed_victory_hand:"
],
- "aliases_ascii": [],
- "keywords": [
- "fu"
- ]
+ "moji": "🖐🏿"
},
"handbag": {
"unicode": "1F45C",
"unicode_alternates": [],
"name": "handbag",
"shortname": ":handbag:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"accessories",
"accessory",
"bag",
- "fashion"
+ "fashion",
+ "women",
+ "vacation"
],
"moji": "👜"
},
- "hard_disk": {
- "unicode": "1F5B4",
+ "handball": {
+ "unicode": "1F93E",
"unicode_alternates": [],
- "name": "hard disk",
- "shortname": ":hard_disk:",
- "category": "objects_symbols",
+ "name": "handball",
+ "shortname": ":handball:",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": [
- "save",
- "technology",
- "storage",
- "information",
- "computer",
- "drive",
- "megabyte",
- "gigabyte",
- "hd"
- ]
+ "keywords": [],
+ "moji": "🤾"
+ },
+ "handball_tone1": {
+ "unicode": "1F93E-1F3FB",
+ "unicode_alternates": [],
+ "name": "handball tone 1",
+ "shortname": ":handball_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤾🏻"
+ },
+ "handball_tone2": {
+ "unicode": "1F93E-1F3FC",
+ "unicode_alternates": [],
+ "name": "handball tone 2",
+ "shortname": ":handball_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤾🏼"
+ },
+ "handball_tone3": {
+ "unicode": "1F93E-1F3FD",
+ "unicode_alternates": [],
+ "name": "handball tone 3",
+ "shortname": ":handball_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤾🏽"
+ },
+ "handball_tone4": {
+ "unicode": "1F93E-1F3FE",
+ "unicode_alternates": [],
+ "name": "handball tone 4",
+ "shortname": ":handball_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤾🏾"
+ },
+ "handball_tone5": {
+ "unicode": "1F93E-1F3FF",
+ "unicode_alternates": [],
+ "name": "handball tone 5",
+ "shortname": ":handball_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤾🏿"
+ },
+ "handshake": {
+ "unicode": "1F91D",
+ "unicode_alternates": [],
+ "name": "handshake",
+ "shortname": ":handshake:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝"
+ },
+ "handshake_tone1": {
+ "unicode": "1F91D-1F3FB",
+ "unicode_alternates": [],
+ "name": "handshake tone 1",
+ "shortname": ":handshake_tone1:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝🏻"
+ },
+ "handshake_tone2": {
+ "unicode": "1F91D-1F3FC",
+ "unicode_alternates": [],
+ "name": "handshake tone 2",
+ "shortname": ":handshake_tone2:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝🏼"
+ },
+ "handshake_tone3": {
+ "unicode": "1F91D-1F3FD",
+ "unicode_alternates": [],
+ "name": "handshake tone 3",
+ "shortname": ":handshake_tone3:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝🏽"
+ },
+ "handshake_tone4": {
+ "unicode": "1F91D-1F3FE",
+ "unicode_alternates": [],
+ "name": "handshake tone 4",
+ "shortname": ":handshake_tone4:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝🏾"
+ },
+ "handshake_tone5": {
+ "unicode": "1F91D-1F3FF",
+ "unicode_alternates": [],
+ "name": "handshake tone 5",
+ "shortname": ":handshake_tone5:",
+ "category": "people",
+ "aliases": [
+ ":shaking_hands_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤝🏿"
},
"hash": {
"moji": "#⃣",
@@ -13999,11 +15560,12 @@
],
"name": "number sign",
"shortname": ":hash:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "symbol"
+ "symbol",
+ "number"
]
},
"hatched_chick": {
@@ -14018,12 +15580,11 @@
"baby",
"chicken",
"chick",
- "baby",
"bird",
- "chicken",
"young",
"woman",
- "cute"
+ "cute",
+ "animal"
],
"moji": "🐥"
},
@@ -14040,19 +15601,18 @@
"chicken",
"egg",
"chick",
- "egg",
"baby",
"bird",
- "chicken",
"young",
"woman",
- "cute"
+ "cute",
+ "animal"
],
"moji": "🐣"
},
"head_bandage": {
"unicode": "1F915",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with head-bandage",
"shortname": ":head_bandage:",
"category": "people",
@@ -14060,14 +15620,20 @@
":face_with_head_bandage:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "smiley",
+ "health",
+ "sick",
+ "emotion"
+ ],
+ "moji": "🤕"
},
"headphones": {
"unicode": "1F3A7",
"unicode_alternates": [],
"name": "headphone",
"shortname": ":headphones:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14076,12 +15642,12 @@
"score",
"headphone",
"sound",
- "music",
"ears",
"beats",
"buds",
"audio",
- "listen"
+ "listen",
+ "instruments"
],
"moji": "🎧"
},
@@ -14090,13 +15656,12 @@
"unicode_alternates": [],
"name": "hear-no-evil monkey",
"shortname": ":hear_no_evil:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"animal",
"monkey",
- "monkey",
"ears",
"hear",
"sound",
@@ -14112,7 +15677,7 @@
],
"name": "heavy black heart",
"shortname": ":heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [
"<3"
@@ -14124,7 +15689,6 @@
"pink",
"black",
"heart",
- "love",
"passion",
"romance",
"intense",
@@ -14132,7 +15696,9 @@
"death",
"evil",
"cold",
- "valentines"
+ "valentines",
+ "symbol",
+ "parties"
]
},
"heart_decoration": {
@@ -14140,19 +15706,20 @@
"unicode_alternates": [],
"name": "heart decoration",
"shortname": ":heart_decoration:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"like",
"love",
- "purple-square"
+ "purple-square",
+ "symbol"
],
"moji": "💟"
},
"heart_exclamation": {
"unicode": "2763",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "heavy heart exclamation mark ornament",
"shortname": ":heart_exclamation:",
"category": "symbols",
@@ -14163,15 +15730,17 @@
"keywords": [
"emotion",
"punctuation",
- "symbol"
- ]
+ "symbol",
+ "love"
+ ],
+ "moji": "❣"
},
"heart_eyes": {
"unicode": "1F60D",
"unicode_alternates": [],
"name": "smiling face with heart-shaped eyes",
"shortname": ":heart_eyes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14185,10 +15754,15 @@
"smiling",
"heart",
"lovestruck",
- "love",
"flirt",
"smile",
- "heart-shaped"
+ "heart-shaped",
+ "happy",
+ "smiley",
+ "sex",
+ "heart eyes",
+ "emotion",
+ "beautiful"
],
"moji": "😍"
},
@@ -14197,7 +15771,7 @@
"unicode_alternates": [],
"name": "smiling cat face with heart-shaped eyes",
"shortname": ":heart_eyes_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14208,41 +15782,27 @@
"love",
"valentines",
"lovestruck",
- "love",
- "heart"
+ "heart",
+ "heart eyes",
+ "cat",
+ "beautiful"
],
"moji": "😻"
},
- "heart_tip": {
- "unicode": "1F394",
- "unicode_alternates": [],
- "name": "heart with tip on the left",
- "shortname": ":heart_tip:",
- "category": "celebration",
- "aliases": [
- ":heart_with_tip_on_the_left:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "affection",
- "like",
- "love",
- "valentines"
- ]
- },
"heartbeat": {
"unicode": "1F493",
"unicode_alternates": [],
"name": "beating heart",
"shortname": ":heartbeat:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"affection",
"like",
"love",
- "valentines"
+ "valentines",
+ "symbol"
],
"moji": "💓"
},
@@ -14251,14 +15811,15 @@
"unicode_alternates": [],
"name": "growing heart",
"shortname": ":heartpulse:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"affection",
"like",
"love",
- "valentines"
+ "valentines",
+ "symbol"
],
"moji": "💗"
},
@@ -14269,12 +15830,15 @@
],
"name": "black heart suit",
"shortname": ":hearts:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cards",
- "poker"
+ "poker",
+ "love",
+ "symbol",
+ "game"
],
"moji": "♥"
},
@@ -14285,12 +15849,13 @@
],
"name": "heavy check mark",
"shortname": ":heavy_check_mark:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"nike",
- "ok"
+ "ok",
+ "symbol"
],
"moji": "✔"
},
@@ -14299,13 +15864,14 @@
"unicode_alternates": [],
"name": "heavy division sign",
"shortname": ":heavy_division_sign:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"calculation",
"divide",
- "math"
+ "math",
+ "symbol"
],
"moji": "➗"
},
@@ -14314,7 +15880,7 @@
"unicode_alternates": [],
"name": "heavy dollar sign",
"shortname": ":heavy_dollar_sign:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14322,12 +15888,12 @@
"money",
"payment",
"dollar",
- "currency",
- "money",
"cash",
"sale",
"purchase",
- "value"
+ "value",
+ "math",
+ "symbol"
],
"moji": "💲"
},
@@ -14336,12 +15902,13 @@
"unicode_alternates": [],
"name": "heavy minus sign",
"shortname": ":heavy_minus_sign:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"calculation",
- "math"
+ "math",
+ "symbol"
],
"moji": "➖"
},
@@ -14352,12 +15919,13 @@
],
"name": "heavy multiplication x",
"shortname": ":heavy_multiplication_x:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"calculation",
- "math"
+ "math",
+ "symbol"
],
"moji": "✖"
},
@@ -14366,12 +15934,13 @@
"unicode_alternates": [],
"name": "heavy plus sign",
"shortname": ":heavy_plus_sign:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"calculation",
- "math"
+ "math",
+ "symbol"
],
"moji": "➕"
},
@@ -14380,7 +15949,7 @@
"unicode_alternates": [],
"name": "helicopter",
"shortname": ":helicopter:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14389,13 +15958,16 @@
"helicopter",
"helo",
"gyro",
- "gyrocopter"
+ "gyrocopter",
+ "plane",
+ "travel",
+ "fly"
],
"moji": "🚁"
},
"helmet_with_cross": {
"unicode": "26D1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "helmet with white cross",
"shortname": ":helmet_with_cross:",
"category": "people",
@@ -14407,8 +15979,12 @@
"aid",
"face",
"hat",
- "person"
- ]
+ "person",
+ "object",
+ "accessories",
+ "job"
+ ],
+ "moji": "⛑"
},
"herb": {
"unicode": "1F33F",
@@ -14427,9 +16003,10 @@
"weed",
"herb",
"spice",
- "plant",
"cook",
- "cooking"
+ "cooking",
+ "nature",
+ "leaf"
],
"moji": "🌿"
},
@@ -14447,7 +16024,9 @@
"vegetable",
"hibiscus",
"flower",
- "warm"
+ "warm",
+ "nature",
+ "tropical"
],
"moji": "🌺"
},
@@ -14456,13 +16035,14 @@
"unicode_alternates": [],
"name": "high brightness symbol",
"shortname": ":high_brightness:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"light",
"summer",
- "sun"
+ "sun",
+ "symbol"
],
"moji": "🔆"
},
@@ -14471,45 +16051,57 @@
"unicode_alternates": [],
"name": "high-heeled shoe",
"shortname": ":high_heel:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fashion",
"female",
- "shoes"
+ "shoes",
+ "women",
+ "shoe",
+ "sexy",
+ "accessories",
+ "girls night"
],
"moji": "👠"
},
"hockey": {
"unicode": "1F3D2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ice hockey stick and puck",
"shortname": ":hockey:",
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "game",
+ "sport",
+ "hockey"
+ ],
+ "moji": "🏒"
},
"hole": {
"unicode": "1F573",
"unicode_alternates": [],
"name": "hole",
"shortname": ":hole:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"pit",
- "well"
- ]
+ "well",
+ "object"
+ ],
+ "moji": "🕳"
},
"homes": {
"unicode": "1F3D8",
"unicode_alternates": [],
"name": "house buildings",
"shortname": ":homes:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":house_buildings:"
],
@@ -14521,15 +16113,19 @@
"mansion",
"bungalow",
"ranch",
- "craftsman"
- ]
+ "craftsman",
+ "places",
+ "building",
+ "house"
+ ],
+ "moji": "🏘"
},
"honey_pot": {
"unicode": "1F36F",
"unicode_alternates": [],
"name": "honey pot",
"shortname": ":honey_pot:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14537,9 +16133,10 @@
"sweet",
"honey",
"pot",
- "bees",
"pooh",
- "bear"
+ "bear",
+ "food",
+ "vagina"
],
"moji": "🍯"
},
@@ -14553,7 +16150,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "brown"
+ "brown",
+ "wildlife"
],
"moji": "🐴"
},
@@ -14562,7 +16160,7 @@
"unicode_alternates": [],
"name": "horse racing",
"shortname": ":horse_racing:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14573,13 +16171,16 @@
"race",
"racing",
"jockey",
- "triple crown"
+ "triple crown",
+ "men",
+ "sport",
+ "horse racing"
],
"moji": "🏇"
},
"horse_racing_tone1": {
"unicode": "1F3C7-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "horse racing tone 1",
"shortname": ":horse_racing_tone1:",
"category": "activity",
@@ -14592,11 +16193,12 @@
"race",
"jockey",
"triple crown"
- ]
+ ],
+ "moji": "🏇🏻"
},
"horse_racing_tone2": {
"unicode": "1F3C7-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "horse racing tone 2",
"shortname": ":horse_racing_tone2:",
"category": "activity",
@@ -14609,11 +16211,12 @@
"race",
"jockey",
"triple crown"
- ]
+ ],
+ "moji": "🏇🏼"
},
"horse_racing_tone3": {
"unicode": "1F3C7-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "horse racing tone 3",
"shortname": ":horse_racing_tone3:",
"category": "activity",
@@ -14626,11 +16229,12 @@
"race",
"jockey",
"triple crown"
- ]
+ ],
+ "moji": "🏇🏽"
},
"horse_racing_tone4": {
"unicode": "1F3C7-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "horse racing tone 4",
"shortname": ":horse_racing_tone4:",
"category": "activity",
@@ -14643,11 +16247,12 @@
"race",
"jockey",
"triple crown"
- ]
+ ],
+ "moji": "🏇🏾"
},
"horse_racing_tone5": {
"unicode": "1F3C7-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "horse racing tone 5",
"shortname": ":horse_racing_tone5:",
"category": "activity",
@@ -14660,21 +16265,24 @@
"race",
"jockey",
"triple crown"
- ]
+ ],
+ "moji": "🏇🏿"
},
"hospital": {
"unicode": "1F3E5",
"unicode_alternates": [],
"name": "hospital",
"shortname": ":hospital:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"building",
"doctor",
"health",
- "surgery"
+ "surgery",
+ "places",
+ "911"
],
"moji": "🏥"
},
@@ -14683,7 +16291,7 @@
"unicode_alternates": [],
"name": "hot pepper",
"shortname": ":hot_pepper:",
- "category": "food_drink",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14693,27 +16301,33 @@
"chili",
"cayenne",
"habanero",
- "jalapeno"
- ]
+ "jalapeno",
+ "vegetables"
+ ],
+ "moji": "🌶"
},
"hotdog": {
"unicode": "1F32D",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "hot dog",
"shortname": ":hotdog:",
- "category": "foods",
+ "category": "food",
"aliases": [
":hot_dog:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "america",
+ "food"
+ ],
+ "moji": "🌭"
},
"hotel": {
"unicode": "1F3E8",
"unicode_alternates": [],
"name": "hotel",
"shortname": ":hotel:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14724,7 +16338,9 @@
"hotel",
"motel",
"holiday inn",
- "hospital"
+ "hospital",
+ "places",
+ "vacation"
],
"moji": "🏨"
},
@@ -14735,13 +16351,14 @@
],
"name": "hot springs",
"shortname": ":hotsprings:",
- "category": "places",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"bath",
"relax",
- "warm"
+ "warm",
+ "symbol"
],
"moji": "♨"
},
@@ -14758,7 +16375,8 @@
"keywords": [
"clock",
"oldschool",
- "time"
+ "time",
+ "object"
],
"moji": "⌛"
},
@@ -14773,7 +16391,8 @@
"keywords": [
"countdown",
"oldschool",
- "time"
+ "time",
+ "object"
],
"moji": "⏳"
},
@@ -14782,20 +16401,20 @@
"unicode_alternates": [],
"name": "house building",
"shortname": ":house:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"building",
"home",
"house",
- "home",
"residence",
"dwelling",
"mansion",
"bungalow",
"ranch",
- "craftsman"
+ "craftsman",
+ "places"
],
"moji": "🏠"
},
@@ -14804,7 +16423,7 @@
"unicode_alternates": [],
"name": "derelict house building",
"shortname": ":house_abandoned:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":derelict_house_building:"
],
@@ -14821,27 +16440,34 @@
"abandoned",
"vacant",
"run down",
- "shoddy"
- ]
+ "shoddy",
+ "places",
+ "building",
+ "house"
+ ],
+ "moji": "🏚"
},
"house_with_garden": {
"unicode": "1F3E1",
"unicode_alternates": [],
"name": "house with garden",
"shortname": ":house_with_garden:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"home",
"nature",
- "plant"
+ "plant",
+ "places",
+ "building",
+ "house"
],
"moji": "🏡"
},
"hugging": {
"unicode": "1F917",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "hugging face",
"shortname": ":hugging:",
"category": "people",
@@ -14849,14 +16475,19 @@
":hugging_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "smiley",
+ "hug",
+ "thank you"
+ ],
+ "moji": "🤗"
},
"hushed": {
"unicode": "1F62F",
"unicode_alternates": [],
"name": "hushed face",
"shortname": ":hushed:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14865,7 +16496,10 @@
"quiet",
"hush",
"whisper",
- "silent"
+ "silent",
+ "smiley",
+ "surprised",
+ "wow"
],
"moji": "😯"
},
@@ -14874,7 +16508,7 @@
"unicode_alternates": [],
"name": "ice cream",
"shortname": ":ice_cream:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14896,7 +16530,7 @@
},
"ice_skate": {
"unicode": "26F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ice skate",
"shortname": ":ice_skate:",
"category": "activity",
@@ -14905,15 +16539,18 @@
"keywords": [
"place",
"sport",
- "travel"
- ]
+ "travel",
+ "cold",
+ "ice skating"
+ ],
+ "moji": "⛸"
},
"icecream": {
"unicode": "1F366",
"unicode_alternates": [],
"name": "soft ice cream",
"shortname": ":icecream:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14935,7 +16572,7 @@
},
"id": {
"unicode": "1F194",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "squared id",
"shortname": ":id:",
"category": "symbols",
@@ -14947,21 +16584,24 @@
"identity",
"symbol",
"word"
- ]
+ ],
+ "moji": "🆔"
},
"ideograph_advantage": {
"unicode": "1F250",
"unicode_alternates": [],
"name": "circled ideograph advantage",
"shortname": ":ideograph_advantage:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"get",
"kanji",
- "obtain"
+ "obtain",
+ "japan",
+ "symbol"
],
"moji": "🉐"
},
@@ -14970,7 +16610,7 @@
"unicode_alternates": [],
"name": "imp",
"shortname": ":imp:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -14979,7 +16619,9 @@
"evil",
"horns",
"cute",
- "devil"
+ "smiley",
+ "monster",
+ "wth"
],
"moji": "👿"
},
@@ -14993,7 +16635,9 @@
"aliases_ascii": [],
"keywords": [
"documents",
- "email"
+ "email",
+ "work",
+ "office"
],
"moji": "📥"
},
@@ -15007,30 +16651,17 @@
"aliases_ascii": [],
"keywords": [
"email",
- "inbox"
+ "inbox",
+ "object"
],
"moji": "📨"
},
- "info": {
- "unicode": "1F6C8",
- "unicode_alternates": [],
- "name": "circled information source",
- "shortname": ":info:",
- "category": "objects_symbols",
- "aliases": [
- ":circled_information_source:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "icon"
- ]
- },
"information_desk_person": {
"unicode": "1F481",
"unicode_alternates": [],
"name": "information desk person",
"shortname": ":information_desk_person:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15045,13 +16676,16 @@
"sassy",
"unimpressed",
"attitude",
- "snarky"
+ "snarky",
+ "people",
+ "women",
+ "diversity"
],
"moji": "💁"
},
"information_desk_person_tone1": {
"unicode": "1F481-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "information desk person tone 1",
"shortname": ":information_desk_person_tone1:",
"category": "people",
@@ -15069,11 +16703,12 @@
"unimpressed",
"attitude",
"snarky"
- ]
+ ],
+ "moji": "💁🏻"
},
"information_desk_person_tone2": {
"unicode": "1F481-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "information desk person tone 2",
"shortname": ":information_desk_person_tone2:",
"category": "people",
@@ -15091,11 +16726,12 @@
"unimpressed",
"attitude",
"snarky"
- ]
+ ],
+ "moji": "💁🏼"
},
"information_desk_person_tone3": {
"unicode": "1F481-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "information desk person tone 3",
"shortname": ":information_desk_person_tone3:",
"category": "people",
@@ -15113,11 +16749,12 @@
"unimpressed",
"attitude",
"snarky"
- ]
+ ],
+ "moji": "💁🏽"
},
"information_desk_person_tone4": {
"unicode": "1F481-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "information desk person tone 4",
"shortname": ":information_desk_person_tone4:",
"category": "people",
@@ -15135,11 +16772,12 @@
"unimpressed",
"attitude",
"snarky"
- ]
+ ],
+ "moji": "💁🏾"
},
"information_desk_person_tone5": {
"unicode": "1F481-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "information desk person tone 5",
"shortname": ":information_desk_person_tone5:",
"category": "people",
@@ -15157,7 +16795,8 @@
"unimpressed",
"attitude",
"snarky"
- ]
+ ],
+ "moji": "💁🏿"
},
"information_source": {
"unicode": "2139",
@@ -15166,13 +16805,14 @@
],
"name": "information source",
"shortname": ":information_source:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"blue-square",
- "letter"
+ "letter",
+ "symbol"
],
"moji": "ℹ"
},
@@ -15181,7 +16821,7 @@
"unicode_alternates": [],
"name": "smiling face with halo",
"shortname": ":innocent:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"O:-)",
@@ -15202,12 +16842,12 @@
"angel",
"face",
"halo",
- "halo",
- "angel",
"innocent",
"ring",
"circle",
- "heaven"
+ "heaven",
+ "smiley",
+ "emotion"
],
"moji": "😇"
},
@@ -15218,13 +16858,14 @@
],
"name": "exclamation question mark",
"shortname": ":interrobang:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"punctuation",
"surprise",
- "wat"
+ "wat",
+ "symbol"
],
"moji": "⁉"
},
@@ -15240,7 +16881,10 @@
"apple",
"dial",
"gadgets",
- "technology"
+ "technology",
+ "electronics",
+ "phone",
+ "selfie"
],
"moji": "📱"
},
@@ -15249,7 +16893,7 @@
"unicode_alternates": [],
"name": "desert island",
"shortname": ":island:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":desert_island:"
],
@@ -15257,15 +16901,22 @@
"keywords": [
"land",
"solitude",
- "alone"
- ]
+ "alone",
+ "places",
+ "travel",
+ "vacation",
+ "tropical",
+ "beach",
+ "swim"
+ ],
+ "moji": "🏝"
},
"izakaya_lantern": {
"unicode": "1F3EE",
"unicode_alternates": [],
"name": "izakaya lantern",
"shortname": ":izakaya_lantern:",
- "category": "places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15277,7 +16928,9 @@
"alcohol",
"bar",
"sake",
- "restaurant"
+ "restaurant",
+ "object",
+ "japan"
],
"moji": "🏮"
},
@@ -15286,14 +16939,13 @@
"unicode_alternates": [],
"name": "jack-o-lantern",
"shortname": ":jack_o_lantern:",
- "category": "objects",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"halloween",
"jack-o-lantern",
"pumpkin",
- "halloween",
"holiday",
"carve",
"autumn",
@@ -15305,7 +16957,8 @@
"horror",
"scary",
"scared",
- "dead"
+ "dead",
+ "holidays"
],
"moji": "🎃"
},
@@ -15314,11 +16967,16 @@
"unicode_alternates": [],
"name": "silhouette of japan",
"shortname": ":japan:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "nation"
+ "nation",
+ "places",
+ "travel",
+ "map",
+ "vacation",
+ "tropical"
],
"moji": "🗾"
},
@@ -15327,7 +16985,7 @@
"unicode_alternates": [],
"name": "japanese castle",
"shortname": ":japanese_castle:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15339,7 +16997,10 @@
"royalty",
"fort",
"fortified",
- "fortress"
+ "fortress",
+ "places",
+ "travel",
+ "vacation"
],
"moji": "🏯"
},
@@ -15348,7 +17009,7 @@
"unicode_alternates": [],
"name": "japanese goblin",
"shortname": ":japanese_goblin:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15361,13 +17022,14 @@
"avian",
"demon",
"goblin",
- "mask",
"theater",
"nose",
"frown",
"mustache",
"anger",
- "frustration"
+ "frustration",
+ "angry",
+ "monster"
],
"moji": "👺"
},
@@ -15376,7 +17038,7 @@
"unicode_alternates": [],
"name": "japanese ogre",
"shortname": ":japanese_ogre:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15387,7 +17049,6 @@
"troll",
"ogre",
"folklore",
- "monster",
"devil",
"mask",
"theater",
@@ -15401,7 +17062,7 @@
"unicode_alternates": [],
"name": "jeans",
"shortname": ":jeans:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15419,26 +17080,12 @@
],
"moji": "👖"
},
- "jet_up": {
- "unicode": "1F6E6",
- "unicode_alternates": [],
- "name": "up-pointing military airplane",
- "shortname": ":jet_up:",
- "category": "travel_places",
- "aliases": [
- ":up_pointing_military_airplane:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "jet"
- ]
- },
"joy": {
"unicode": "1F602",
"unicode_alternates": [],
"name": "face with tears of joy",
"shortname": ":joy:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":')",
@@ -15450,11 +17097,13 @@
"haha",
"happy",
"tears",
- "tears",
- "cry",
"joy",
- "happy",
- "weep"
+ "weep",
+ "silly",
+ "smiley",
+ "laugh",
+ "emotion",
+ "sarcastic"
],
"moji": "😂"
},
@@ -15463,7 +17112,7 @@
"unicode_alternates": [],
"name": "cat face with tears of joy",
"shortname": ":joy_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15472,10 +17121,12 @@
"haha",
"happy",
"tears",
- "happy",
- "tears",
"cry",
- "joy"
+ "joy",
+ "silly",
+ "laugh",
+ "cat",
+ "sarcastic"
],
"moji": "😹"
},
@@ -15484,24 +17135,112 @@
"unicode_alternates": [],
"name": "joystick",
"shortname": ":joystick:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"games",
"atari",
- "controller"
- ]
+ "controller",
+ "electronics",
+ "game",
+ "boys night"
+ ],
+ "moji": "🕹"
+ },
+ "juggling": {
+ "unicode": "1F939",
+ "unicode_alternates": [],
+ "name": "juggling",
+ "shortname": ":juggling:",
+ "category": "activity",
+ "aliases": [
+ ":juggler:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹"
+ },
+ "juggling_tone1": {
+ "unicode": "1F939-1F3FB",
+ "unicode_alternates": [],
+ "name": "juggling tone 1",
+ "shortname": ":juggling_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":juggler_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹🏻"
+ },
+ "juggling_tone2": {
+ "unicode": "1F939-1F3FC",
+ "unicode_alternates": [],
+ "name": "juggling tone 2",
+ "shortname": ":juggling_tone2:",
+ "category": "activity",
+ "aliases": [
+ ":juggler_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹🏼"
+ },
+ "juggling_tone3": {
+ "unicode": "1F939-1F3FD",
+ "unicode_alternates": [],
+ "name": "juggling tone 3",
+ "shortname": ":juggling_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":juggler_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹🏽"
+ },
+ "juggling_tone4": {
+ "unicode": "1F939-1F3FE",
+ "unicode_alternates": [],
+ "name": "juggling tone 4",
+ "shortname": ":juggling_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":juggler_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹🏾"
+ },
+ "juggling_tone5": {
+ "unicode": "1F939-1F3FF",
+ "unicode_alternates": [],
+ "name": "juggling tone 5",
+ "shortname": ":juggling_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":juggler_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤹🏿"
},
"kaaba": {
"unicode": "1F54B",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "kaaba",
"shortname": ":kaaba:",
"category": "travel",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "places",
+ "religion",
+ "building",
+ "condolence"
+ ],
+ "moji": "🕋"
},
"key": {
"unicode": "1F511",
@@ -15514,7 +17253,8 @@
"keywords": [
"door",
"lock",
- "password"
+ "password",
+ "object"
],
"moji": "🔑"
},
@@ -15523,7 +17263,7 @@
"unicode_alternates": [],
"name": "old key",
"shortname": ":key2:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":old_key:"
],
@@ -15532,79 +17272,34 @@
"door",
"lock",
"password",
- "skeleton"
- ]
- },
- "keyboard": {
- "unicode": "1F5AE",
- "unicode_alternates": [],
- "name": "wired keyboard",
- "shortname": ":keyboard:",
- "category": "objects_symbols",
- "aliases": [
- ":wired_keyboard:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "typing",
- "keys",
- "input",
- "device"
- ]
- },
- "keyboard_mouse": {
- "unicode": "1F5A6",
- "unicode_alternates": [],
- "name": "keyboard and mouse",
- "shortname": ":keyboard_mouse:",
- "category": "objects_symbols",
- "aliases": [
- ":keyboard_and_mouse:"
+ "skeleton",
+ "object"
],
- "aliases_ascii": [],
- "keywords": [
- "computer",
- "input",
- "desktop"
- ]
+ "moji": "🗝"
},
- "keyboard_with_jacks": {
- "unicode": "1F398",
- "unicode_alternates": [],
- "name": "musical keyboard with jacks",
- "shortname": ":keyboard_with_jacks:",
- "category": "objects_symbols",
- "aliases": [
- ":musical_keyboard_with_jacks:"
+ "keyboard": {
+ "unicode": "2328",
+ "unicode_alternates": [
+ "2328-FE0F"
],
- "aliases_ascii": [],
- "keywords": [
- "music",
- "instrument",
- "midi"
- ]
- },
- "keycap_ten": {
- "unicode": "1F51F",
- "unicode_alternates": [],
- "name": "keycap ten",
- "shortname": ":keycap_ten:",
- "category": "other",
+ "name": "keyboard",
+ "shortname": ":keyboard:",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "10",
- "blue-square",
- "numbers"
+ "electronics",
+ "work",
+ "office"
],
- "moji": "🔟"
+ "moji": "⌨"
},
"kimono": {
"unicode": "1F458",
"unicode_alternates": [],
"name": "kimono",
"shortname": ":kimono:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15621,7 +17316,7 @@
"unicode_alternates": [],
"name": "kiss mark",
"shortname": ":kiss:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15630,7 +17325,12 @@
"like",
"lips",
"love",
- "valentines"
+ "valentines",
+ "women",
+ "sexy",
+ "lip",
+ "beautiful",
+ "girls night"
],
"moji": "💋"
},
@@ -15652,8 +17352,14 @@
"love",
"marriage",
"valentines",
- "couple"
- ]
+ "couple",
+ "people",
+ "gay",
+ "men",
+ "sex",
+ "lgbt"
+ ],
+ "moji": "👨‍❤️‍💋‍👨"
},
"kiss_ww": {
"unicode": "1F469-2764-1F48B-1F469",
@@ -15673,15 +17379,21 @@
"love",
"marriage",
"valentines",
- "couple"
- ]
+ "couple",
+ "people",
+ "women",
+ "sex",
+ "lgbt",
+ "lesbian"
+ ],
+ "moji": "👩‍❤️‍💋‍👩"
},
"kissing": {
"unicode": "1F617",
"unicode_alternates": [],
"name": "kissing face",
"shortname": ":kissing:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15695,7 +17407,9 @@
"kiss",
"pucker",
"lips",
- "smooch"
+ "smooch",
+ "smiley",
+ "sexy"
],
"moji": "😗"
},
@@ -15704,7 +17418,7 @@
"unicode_alternates": [],
"name": "kissing cat face with closed eyes",
"shortname": ":kissing_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15714,7 +17428,8 @@
"kiss",
"puckered",
"heart",
- "love"
+ "love",
+ "cat"
],
"moji": "😽"
},
@@ -15723,7 +17438,7 @@
"unicode_alternates": [],
"name": "kissing face with closed eyes",
"shortname": ":kissing_closed_eyes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15738,8 +17453,9 @@
"passion",
"puckered",
"heart",
- "love",
- "smooch"
+ "smooch",
+ "smiley",
+ "sexy"
],
"moji": "😚"
},
@@ -15748,7 +17464,7 @@
"unicode_alternates": [],
"name": "face throwing a kiss",
"shortname": ":kissing_heart:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":*",
@@ -15766,8 +17482,9 @@
"love",
"lips",
"like",
- "love",
- "valentines"
+ "valentines",
+ "smiley",
+ "sexy"
],
"moji": "😘"
},
@@ -15776,7 +17493,7 @@
"unicode_alternates": [],
"name": "kissing face with smiling eyes",
"shortname": ":kissing_smiling_eyes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15789,10 +17506,25 @@
"smile",
"pucker",
"lips",
- "smooch"
+ "smooch",
+ "smiley",
+ "sexy"
],
"moji": "😙"
},
+ "kiwi": {
+ "unicode": "1F95D",
+ "unicode_alternates": [],
+ "name": "kiwifruit",
+ "shortname": ":kiwi:",
+ "category": "food",
+ "aliases": [
+ ":kiwifruit:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥝"
+ },
"knife": {
"unicode": "1F52A",
"unicode_alternates": [],
@@ -15801,7 +17533,10 @@
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "object",
+ "weapon"
+ ],
"moji": "🔪"
},
"koala": {
@@ -15814,7 +17549,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐨"
},
@@ -15823,7 +17559,7 @@
"unicode_alternates": [],
"name": "squared katakana koko",
"shortname": ":koko:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -15831,7 +17567,8 @@
"destination",
"here",
"japanese",
- "katakana"
+ "katakana",
+ "symbol"
],
"moji": "🈁"
},
@@ -15840,22 +17577,28 @@
"unicode_alternates": [],
"name": "label",
"shortname": ":label:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "tag"
- ]
+ "tag",
+ "object"
+ ],
+ "moji": "🏷"
},
"large_blue_circle": {
"unicode": "1F535",
"unicode_alternates": [],
"name": "large blue circle",
"shortname": ":large_blue_circle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "shapes",
+ "symbol",
+ "circle"
+ ],
"moji": "🔵"
},
"large_blue_diamond": {
@@ -15863,11 +17606,13 @@
"unicode_alternates": [],
"name": "large blue diamond",
"shortname": ":large_blue_diamond:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol"
],
"moji": "🔷"
},
@@ -15876,11 +17621,13 @@
"unicode_alternates": [],
"name": "large orange diamond",
"shortname": ":large_orange_diamond:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol"
],
"moji": "🔶"
},
@@ -15900,7 +17647,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌗"
},
@@ -15922,7 +17670,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌜"
},
@@ -15931,7 +17680,7 @@
"unicode_alternates": [],
"name": "smiling face with open mouth and tightly-closed ey",
"shortname": ":laughing:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":satisfied:"
],
@@ -15947,7 +17696,9 @@
"lol",
"smiling",
"laughing",
- "laugh"
+ "laugh",
+ "smiley",
+ "emotion"
],
"moji": "😆"
},
@@ -15984,16 +17735,97 @@
"aliases_ascii": [],
"keywords": [
"notes",
- "paper"
+ "paper",
+ "object",
+ "office",
+ "write"
],
"moji": "📒"
},
+ "left_facing_fist": {
+ "unicode": "1F91B",
+ "unicode_alternates": [],
+ "name": "left-facing fist",
+ "shortname": ":left_facing_fist:",
+ "category": "people",
+ "aliases": [
+ ":left_fist:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛"
+ },
+ "left_facing_fist_tone1": {
+ "unicode": "1F91B-1F3FB",
+ "unicode_alternates": [],
+ "name": "left facing fist tone 1",
+ "shortname": ":left_facing_fist_tone1:",
+ "category": "people",
+ "aliases": [
+ ":left_fist_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛🏻"
+ },
+ "left_facing_fist_tone2": {
+ "unicode": "1F91B-1F3FC",
+ "unicode_alternates": [],
+ "name": "left facing fist tone 2",
+ "shortname": ":left_facing_fist_tone2:",
+ "category": "people",
+ "aliases": [
+ ":left_fist_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛🏼"
+ },
+ "left_facing_fist_tone3": {
+ "unicode": "1F91B-1F3FD",
+ "unicode_alternates": [],
+ "name": "left facing fist tone 3",
+ "shortname": ":left_facing_fist_tone3:",
+ "category": "people",
+ "aliases": [
+ ":left_fist_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛🏽"
+ },
+ "left_facing_fist_tone4": {
+ "unicode": "1F91B-1F3FE",
+ "unicode_alternates": [],
+ "name": "left facing fist tone 4",
+ "shortname": ":left_facing_fist_tone4:",
+ "category": "people",
+ "aliases": [
+ ":left_fist_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛🏾"
+ },
+ "left_facing_fist_tone5": {
+ "unicode": "1F91B-1F3FF",
+ "unicode_alternates": [],
+ "name": "left facing fist tone 5",
+ "shortname": ":left_facing_fist_tone5:",
+ "category": "people",
+ "aliases": [
+ ":left_fist_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤛🏿"
+ },
"left_luggage": {
"unicode": "1F6C5",
"unicode_alternates": [],
"name": "left luggage",
"shortname": ":left_luggage:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16002,26 +17834,10 @@
"bag",
"baggage",
"luggage",
- "travel"
+ "symbol"
],
"moji": "🛅"
},
- "left_receiver": {
- "unicode": "1F57B",
- "unicode_alternates": [],
- "name": "left hand telephone receiver",
- "shortname": ":left_receiver:",
- "category": "objects_symbols",
- "aliases": [
- ":left_hand_telephone_receiver:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "dial",
- "technology"
- ]
- },
"left_right_arrow": {
"unicode": "2194",
"unicode_alternates": [
@@ -16029,11 +17845,13 @@
],
"name": "left right arrow",
"shortname": ":left_right_arrow:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "arrow",
+ "symbol"
],
"moji": "↔"
},
@@ -16044,10 +17862,13 @@
],
"name": "leftwards arrow with hook",
"shortname": ":leftwards_arrow_with_hook:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "arrow",
+ "symbol"
+ ],
"moji": "↩"
},
"lemon": {
@@ -16055,7 +17876,7 @@
"unicode_alternates": [],
"name": "lemon",
"shortname": ":lemon:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16063,7 +17884,8 @@
"nature",
"lemon",
"yellow",
- "citrus"
+ "citrus",
+ "food"
],
"moji": "🍋"
},
@@ -16074,7 +17896,7 @@
],
"name": "leo",
"shortname": ":leo:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16087,9 +17909,8 @@
"zodiac",
"sign",
"purple-square",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♌"
},
@@ -16108,7 +17929,9 @@
"cat",
"spot",
"spotted",
- "sexy"
+ "sexy",
+ "wildlife",
+ "roar"
],
"moji": "🐆"
},
@@ -16117,27 +17940,31 @@
"unicode_alternates": [],
"name": "level slider",
"shortname": ":level_slider:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"controls"
- ]
+ ],
+ "moji": "🎚"
},
"levitate": {
"unicode": "1F574",
"unicode_alternates": [],
"name": "man in business suit levitating",
"shortname": ":levitate:",
- "category": "people",
+ "category": "activity",
"aliases": [
":man_in_business_suit_levitating:"
],
"aliases_ascii": [],
"keywords": [
"hover",
- "exclamation"
- ]
+ "exclamation",
+ "men",
+ "job"
+ ],
+ "moji": "🕴"
},
"libra": {
"unicode": "264E",
@@ -16146,7 +17973,7 @@
],
"name": "libra",
"shortname": ":libra:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16159,9 +17986,8 @@
"zodiac",
"sign",
"purple-square",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♎"
},
@@ -16179,12 +18005,20 @@
"bench",
"press",
"squats",
- "deadlift"
- ]
+ "deadlift",
+ "men",
+ "workout",
+ "flex",
+ "sport",
+ "weight lifting",
+ "win",
+ "diversity"
+ ],
+ "moji": "🏋"
},
"lifter_tone1": {
"unicode": "1F3CB-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "weight lifter tone 1",
"shortname": ":lifter_tone1:",
"category": "activity",
@@ -16197,11 +18031,12 @@
"press",
"squats",
"deadlift"
- ]
+ ],
+ "moji": "🏋🏻"
},
"lifter_tone2": {
"unicode": "1F3CB-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "weight lifter tone 2",
"shortname": ":lifter_tone2:",
"category": "activity",
@@ -16214,11 +18049,12 @@
"press",
"squats",
"deadlift"
- ]
+ ],
+ "moji": "🏋🏼"
},
"lifter_tone3": {
"unicode": "1F3CB-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "weight lifter tone 3",
"shortname": ":lifter_tone3:",
"category": "activity",
@@ -16231,11 +18067,12 @@
"press",
"squats",
"deadlift"
- ]
+ ],
+ "moji": "🏋🏽"
},
"lifter_tone4": {
"unicode": "1F3CB-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "weight lifter tone 4",
"shortname": ":lifter_tone4:",
"category": "activity",
@@ -16248,11 +18085,12 @@
"press",
"squats",
"deadlift"
- ]
+ ],
+ "moji": "🏋🏾"
},
"lifter_tone5": {
"unicode": "1F3CB-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "weight lifter tone 5",
"shortname": ":lifter_tone5:",
"category": "activity",
@@ -16265,28 +18103,15 @@
"press",
"squats",
"deadlift"
- ]
- },
- "light_check_mark": {
- "unicode": "1F5F8",
- "unicode_alternates": [],
- "name": "light check mark",
- "shortname": ":light_check_mark:",
- "category": "objects_symbols",
- "aliases": [
- ":light_mark:"
],
- "aliases_ascii": [],
- "keywords": [
- "vote"
- ]
+ "moji": "🏋🏿"
},
"light_rail": {
"unicode": "1F688",
"unicode_alternates": [],
"name": "light rail",
"shortname": ":light_rail:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16294,7 +18119,8 @@
"vehicle",
"train",
"rail",
- "light"
+ "light",
+ "travel"
],
"moji": "🚈"
},
@@ -16303,18 +18129,20 @@
"unicode_alternates": [],
"name": "link symbol",
"shortname": ":link:",
- "category": "other",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"rings",
- "url"
+ "url",
+ "symbol",
+ "office"
],
"moji": "🔗"
},
"lion_face": {
"unicode": "1F981",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "lion face",
"shortname": ":lion_face:",
"category": "nature",
@@ -16322,50 +18150,62 @@
":lion:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "wildlife",
+ "roar",
+ "cat",
+ "animal"
+ ],
+ "moji": "🦁"
},
"lips": {
"unicode": "1F444",
"unicode_alternates": [],
"name": "mouth",
"shortname": ":lips:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"kiss",
- "mouth"
+ "mouth",
+ "women",
+ "body",
+ "sexy",
+ "lip"
],
"moji": "👄"
},
- "lips2": {
- "unicode": "1F5E2",
- "unicode_alternates": [],
- "name": "lips",
- "shortname": ":lips2:",
- "category": "people",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "kiss",
- "mouth"
- ]
- },
"lipstick": {
"unicode": "1F484",
"unicode_alternates": [],
"name": "lipstick",
"shortname": ":lipstick:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fashion",
"female",
- "girl"
+ "girl",
+ "object",
+ "women",
+ "sexy",
+ "lip"
],
"moji": "💄"
},
+ "lizard": {
+ "unicode": "1F98E",
+ "unicode_alternates": [],
+ "name": "lizard",
+ "shortname": ":lizard:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦎"
+ },
"lock": {
"unicode": "1F512",
"unicode_alternates": [],
@@ -16376,7 +18216,9 @@
"aliases_ascii": [],
"keywords": [
"password",
- "security"
+ "security",
+ "object",
+ "lock"
],
"moji": "🔒"
},
@@ -16390,7 +18232,9 @@
"aliases_ascii": [],
"keywords": [
"secret",
- "security"
+ "security",
+ "object",
+ "lock"
],
"moji": "🔏"
},
@@ -16399,7 +18243,7 @@
"unicode_alternates": [],
"name": "lollipop",
"shortname": ":lollipop:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16410,9 +18254,8 @@
"lollipop",
"stick",
"lick",
- "sweet",
"sugar",
- "candy"
+ "halloween"
],
"moji": "🍭"
},
@@ -16421,11 +18264,12 @@
"unicode_alternates": [],
"name": "double curly loop",
"shortname": ":loop:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "curly"
+ "curly",
+ "symbol"
],
"moji": "➿"
},
@@ -16434,10 +18278,13 @@
"unicode_alternates": [],
"name": "speaker with three sound waves",
"shortname": ":loud_sound:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": [],
+ "keywords": [
+ "alarm",
+ "symbol"
+ ],
"moji": "🔊"
},
"loudspeaker": {
@@ -16445,12 +18292,15 @@
"unicode_alternates": [],
"name": "public address loudspeaker",
"shortname": ":loudspeaker:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"sound",
- "volume"
+ "volume",
+ "object",
+ "alarm",
+ "symbol"
],
"moji": "📢"
},
@@ -16459,7 +18309,7 @@
"unicode_alternates": [],
"name": "love hotel",
"shortname": ":love_hotel:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16468,7 +18318,6 @@
"like",
"love",
"hotel",
- "love",
"sex",
"romance",
"leisure",
@@ -16476,7 +18325,9 @@
"prostitution",
"hospital",
"birth",
- "happy"
+ "happy",
+ "places",
+ "building"
],
"moji": "🏩"
},
@@ -16485,7 +18336,7 @@
"unicode_alternates": [],
"name": "love letter",
"shortname": ":love_letter:",
- "category": "emoticons",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16497,7 +18348,8 @@
"love",
"letter",
"kiss",
- "heart"
+ "heart",
+ "object"
],
"moji": "💌"
},
@@ -16506,15 +18358,29 @@
"unicode_alternates": [],
"name": "low brightness symbol",
"shortname": ":low_brightness:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"summer",
- "sun"
+ "sun",
+ "symbol"
],
"moji": "🔅"
},
+ "lying_face": {
+ "unicode": "1F925",
+ "unicode_alternates": [],
+ "name": "lying face",
+ "shortname": ":lying_face:",
+ "category": "people",
+ "aliases": [
+ ":liar:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤥"
+ },
"m": {
"unicode": "24C2",
"unicode_alternates": [
@@ -16522,13 +18388,14 @@
],
"name": "circled latin capital letter m",
"shortname": ":m:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"blue-circle",
- "letter"
+ "letter",
+ "symbol"
],
"moji": "Ⓜ"
},
@@ -16546,7 +18413,8 @@
"detective",
"investigator",
"detail",
- "details"
+ "details",
+ "object"
],
"moji": "🔍"
},
@@ -16564,7 +18432,8 @@
"detective",
"investigator",
"detail",
- "details"
+ "details",
+ "object"
],
"moji": "🔎"
},
@@ -16575,13 +18444,15 @@
],
"name": "mahjong tile red dragon",
"shortname": ":mahjong:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"game",
- "kanji"
+ "kanji",
+ "object",
+ "symbol"
],
"moji": "🀄"
},
@@ -16596,7 +18467,8 @@
"keywords": [
"communication",
"email",
- "inbox"
+ "inbox",
+ "object"
],
"moji": "📫"
},
@@ -16611,7 +18483,9 @@
"keywords": [
"communication",
"email",
- "inbox"
+ "inbox",
+ "object",
+ "office"
],
"moji": "📪"
},
@@ -16626,7 +18500,8 @@
"keywords": [
"communication",
"email",
- "inbox"
+ "inbox",
+ "object"
],
"moji": "📬"
},
@@ -16640,7 +18515,8 @@
"aliases_ascii": [],
"keywords": [
"email",
- "inbox"
+ "inbox",
+ "object"
],
"moji": "📭"
},
@@ -16649,7 +18525,7 @@
"unicode_alternates": [],
"name": "man",
"shortname": ":man:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16657,13 +18533,173 @@
"dad",
"father",
"guy",
- "mustashe"
+ "mustashe",
+ "people",
+ "men",
+ "sex",
+ "diversity",
+ "selfie",
+ "boys night"
],
"moji": "👨"
},
+ "man_dancing": {
+ "unicode": "1F57A",
+ "unicode_alternates": [],
+ "name": "man dancing",
+ "shortname": ":man_dancing:",
+ "category": "people",
+ "aliases": [
+ ":male_dancer:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺"
+ },
+ "man_dancing_tone1": {
+ "unicode": "1F57A-1F3FB",
+ "unicode_alternates": [],
+ "name": "man dancing tone 1",
+ "shortname": ":man_dancing_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":male_dancer_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺🏻"
+ },
+ "man_dancing_tone2": {
+ "unicode": "1F57A-1F3FC",
+ "unicode_alternates": [],
+ "name": "man dancing tone 2",
+ "shortname": ":man_dancing_tone2:",
+ "category": "activity",
+ "aliases": [
+ ":male_dancer_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺🏼"
+ },
+ "man_dancing_tone3": {
+ "unicode": "1F57A-1F3FD",
+ "unicode_alternates": [],
+ "name": "man dancing tone 3",
+ "shortname": ":man_dancing_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":male_dancer_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺🏽"
+ },
+ "man_dancing_tone4": {
+ "unicode": "1F57A-1F3FE",
+ "unicode_alternates": [],
+ "name": "man dancing tone 4",
+ "shortname": ":man_dancing_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":male_dancer_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺🏾"
+ },
+ "man_dancing_tone5": {
+ "unicode": "1F57A-1F3FF",
+ "unicode_alternates": [],
+ "name": "man dancing tone 5",
+ "shortname": ":man_dancing_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":male_dancer_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🕺🏿"
+ },
+ "man_in_tuxedo": {
+ "unicode": "1F935",
+ "unicode_alternates": [],
+ "name": "man in tuxedo",
+ "shortname": ":man_in_tuxedo:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵"
+ },
+ "man_in_tuxedo_tone1": {
+ "unicode": "1F935-1F3FB",
+ "unicode_alternates": [],
+ "name": "man in tuxedo tone 1",
+ "shortname": ":man_in_tuxedo_tone1:",
+ "category": "people",
+ "aliases": [
+ ":tuxedo_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵🏻"
+ },
+ "man_in_tuxedo_tone2": {
+ "unicode": "1F935-1F3FC",
+ "unicode_alternates": [],
+ "name": "man in tuxedo tone 2",
+ "shortname": ":man_in_tuxedo_tone2:",
+ "category": "people",
+ "aliases": [
+ ":tuxedo_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵🏼"
+ },
+ "man_in_tuxedo_tone3": {
+ "unicode": "1F935-1F3FD",
+ "unicode_alternates": [],
+ "name": "man in tuxedo tone 3",
+ "shortname": ":man_in_tuxedo_tone3:",
+ "category": "people",
+ "aliases": [
+ ":tuxedo_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵🏽"
+ },
+ "man_in_tuxedo_tone4": {
+ "unicode": "1F935-1F3FE",
+ "unicode_alternates": [],
+ "name": "man in tuxedo tone 4",
+ "shortname": ":man_in_tuxedo_tone4:",
+ "category": "people",
+ "aliases": [
+ ":tuxedo_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵🏾"
+ },
+ "man_in_tuxedo_tone5": {
+ "unicode": "1F935-1F3FF",
+ "unicode_alternates": [],
+ "name": "man in tuxedo tone 5",
+ "shortname": ":man_in_tuxedo_tone5:",
+ "category": "people",
+ "aliases": [
+ ":tuxedo_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤵🏿"
+ },
"man_tone1": {
"unicode": "1F468-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man tone 1",
"shortname": ":man_tone1:",
"category": "people",
@@ -16675,11 +18711,12 @@
"father",
"guy",
"mustache"
- ]
+ ],
+ "moji": "👨🏻"
},
"man_tone2": {
"unicode": "1F468-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man tone 2",
"shortname": ":man_tone2:",
"category": "people",
@@ -16691,11 +18728,12 @@
"father",
"guy",
"mustache"
- ]
+ ],
+ "moji": "👨🏼"
},
"man_tone3": {
"unicode": "1F468-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man tone 3",
"shortname": ":man_tone3:",
"category": "people",
@@ -16707,11 +18745,12 @@
"father",
"guy",
"mustache"
- ]
+ ],
+ "moji": "👨🏽"
},
"man_tone4": {
"unicode": "1F468-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man tone 4",
"shortname": ":man_tone4:",
"category": "people",
@@ -16723,11 +18762,12 @@
"father",
"guy",
"mustache"
- ]
+ ],
+ "moji": "👨🏾"
},
"man_tone5": {
"unicode": "1F468-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man tone 5",
"shortname": ":man_tone5:",
"category": "people",
@@ -16739,14 +18779,15 @@
"father",
"guy",
"mustache"
- ]
+ ],
+ "moji": "👨🏿"
},
"man_with_gua_pi_mao": {
"unicode": "1F472",
"unicode_alternates": [],
"name": "man with gua pi mao",
"shortname": ":man_with_gua_pi_mao:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16755,13 +18796,17 @@
"skullcap",
"chinese",
"asian",
- "qing"
+ "qing",
+ "people",
+ "hat",
+ "men",
+ "diversity"
],
"moji": "👲"
},
"man_with_gua_pi_mao_tone1": {
"unicode": "1F472-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with gua pi mao tone 1",
"shortname": ":man_with_gua_pi_mao_tone1:",
"category": "people",
@@ -16774,11 +18819,12 @@
"chinese",
"asian",
"qing"
- ]
+ ],
+ "moji": "👲🏻"
},
"man_with_gua_pi_mao_tone2": {
"unicode": "1F472-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with gua pi mao tone 2",
"shortname": ":man_with_gua_pi_mao_tone2:",
"category": "people",
@@ -16791,11 +18837,12 @@
"chinese",
"asian",
"qing"
- ]
+ ],
+ "moji": "👲🏼"
},
"man_with_gua_pi_mao_tone3": {
"unicode": "1F472-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with gua pi mao tone 3",
"shortname": ":man_with_gua_pi_mao_tone3:",
"category": "people",
@@ -16808,11 +18855,12 @@
"chinese",
"asian",
"qing"
- ]
+ ],
+ "moji": "👲🏽"
},
"man_with_gua_pi_mao_tone4": {
"unicode": "1F472-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with gua pi mao tone 4",
"shortname": ":man_with_gua_pi_mao_tone4:",
"category": "people",
@@ -16825,11 +18873,12 @@
"chinese",
"asian",
"qing"
- ]
+ ],
+ "moji": "👲🏾"
},
"man_with_gua_pi_mao_tone5": {
"unicode": "1F472-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with gua pi mao tone 5",
"shortname": ":man_with_gua_pi_mao_tone5:",
"category": "people",
@@ -16842,14 +18891,15 @@
"chinese",
"asian",
"qing"
- ]
+ ],
+ "moji": "👲🏿"
},
"man_with_turban": {
"unicode": "1F473",
"unicode_alternates": [],
"name": "man with turban",
"shortname": ":man_with_turban:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -16862,13 +18912,16 @@
"indian",
"mummy",
"wisdom",
- "peace"
+ "peace",
+ "people",
+ "hat",
+ "diversity"
],
"moji": "👳"
},
"man_with_turban_tone1": {
"unicode": "1F473-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with turban tone 1",
"shortname": ":man_with_turban_tone1:",
"category": "people",
@@ -16884,11 +18937,12 @@
"mummy",
"wisdom",
"peace"
- ]
+ ],
+ "moji": "👳🏻"
},
"man_with_turban_tone2": {
"unicode": "1F473-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with turban tone 2",
"shortname": ":man_with_turban_tone2:",
"category": "people",
@@ -16904,11 +18958,12 @@
"mummy",
"wisdom",
"peace"
- ]
+ ],
+ "moji": "👳🏼"
},
"man_with_turban_tone3": {
"unicode": "1F473-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with turban tone 3",
"shortname": ":man_with_turban_tone3:",
"category": "people",
@@ -16924,11 +18979,12 @@
"mummy",
"wisdom",
"peace"
- ]
+ ],
+ "moji": "👳🏽"
},
"man_with_turban_tone4": {
"unicode": "1F473-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with turban tone 4",
"shortname": ":man_with_turban_tone4:",
"category": "people",
@@ -16944,11 +19000,12 @@
"mummy",
"wisdom",
"peace"
- ]
+ ],
+ "moji": "👳🏾"
},
"man_with_turban_tone5": {
"unicode": "1F473-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "man with turban tone 5",
"shortname": ":man_with_turban_tone5:",
"category": "people",
@@ -16964,19 +19021,22 @@
"mummy",
"wisdom",
"peace"
- ]
+ ],
+ "moji": "👳🏿"
},
"mans_shoe": {
"unicode": "1F45E",
"unicode_alternates": [],
"name": "mans shoe",
"shortname": ":mans_shoe:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fashion",
- "male"
+ "male",
+ "shoe",
+ "accessories"
],
"moji": "👞"
},
@@ -16985,7 +19045,7 @@
"unicode_alternates": [],
"name": "world map",
"shortname": ":map:",
- "category": "travel_places",
+ "category": "objects",
"aliases": [
":world_map:"
],
@@ -16993,8 +19053,12 @@
"keywords": [
"atlas",
"earth",
- "cartography"
- ]
+ "cartography",
+ "travel",
+ "map",
+ "vacation"
+ ],
+ "moji": "🗺"
},
"maple_leaf": {
"unicode": "1F341",
@@ -17012,28 +19076,42 @@
"maple",
"leaf",
"syrup",
- "canada",
"tree"
],
"moji": "🍁"
},
+ "martial_arts_uniform": {
+ "unicode": "1F94B",
+ "unicode_alternates": [],
+ "name": "martial arts uniform",
+ "shortname": ":martial_arts_uniform:",
+ "category": "activity",
+ "aliases": [
+ ":karate_uniform:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥋"
+ },
"mask": {
"unicode": "1F637",
"unicode_alternates": [],
"name": "face with medical mask",
"shortname": ":mask:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"face",
"ill",
"sick",
- "sick",
"virus",
"flu",
"medical",
- "mask"
+ "mask",
+ "smiley",
+ "dead",
+ "health"
],
"moji": "😷"
},
@@ -17042,19 +19120,22 @@
"unicode_alternates": [],
"name": "face massage",
"shortname": ":massage:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"female",
"girl",
- "woman"
+ "woman",
+ "people",
+ "women",
+ "diversity"
],
"moji": "💆"
},
"massage_tone1": {
"unicode": "1F486-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face massage tone 1",
"shortname": ":massage_tone1:",
"category": "people",
@@ -17064,11 +19145,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💆🏻"
},
"massage_tone2": {
"unicode": "1F486-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face massage tone 2",
"shortname": ":massage_tone2:",
"category": "people",
@@ -17078,11 +19160,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💆🏼"
},
"massage_tone3": {
"unicode": "1F486-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face massage tone 3",
"shortname": ":massage_tone3:",
"category": "people",
@@ -17092,11 +19175,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💆🏽"
},
"massage_tone4": {
"unicode": "1F486-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face massage tone 4",
"shortname": ":massage_tone4:",
"category": "people",
@@ -17106,11 +19190,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💆🏾"
},
"massage_tone5": {
"unicode": "1F486-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face massage tone 5",
"shortname": ":massage_tone5:",
"category": "people",
@@ -17120,14 +19205,15 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "💆🏿"
},
"meat_on_bone": {
"unicode": "1F356",
"unicode_alternates": [],
"name": "meat on bone",
"shortname": ":meat_on_bone:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17160,21 +19246,27 @@
"first",
"show",
"reward",
- "achievement"
- ]
+ "achievement",
+ "object",
+ "sport",
+ "perfect"
+ ],
+ "moji": "🏅"
},
"mega": {
"unicode": "1F4E3",
"unicode_alternates": [],
"name": "cheering megaphone",
"shortname": ":mega:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"sound",
"speaker",
- "volume"
+ "volume",
+ "object",
+ "sport"
],
"moji": "📣"
},
@@ -17183,7 +19275,7 @@
"unicode_alternates": [],
"name": "melon",
"shortname": ":melon:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17192,26 +19284,34 @@
"nature",
"melon",
"cantaloupe",
- "honeydew"
+ "honeydew",
+ "boobs"
],
"moji": "🍈"
},
"menorah": {
"unicode": "1F54E",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "menorah with nine branches",
"shortname": ":menorah:",
"category": "symbols",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "religion",
+ "object",
+ "jew",
+ "symbol",
+ "holidays"
+ ],
+ "moji": "🕎"
},
"mens": {
"unicode": "1F6B9",
"unicode_alternates": [],
"name": "mens symbol",
"shortname": ":mens:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17220,17 +19320,17 @@
"wc",
"men",
"bathroom",
- "restroom",
"sign",
"boy",
"male",
- "avatar"
+ "avatar",
+ "symbol"
],
"moji": "🚹"
},
"metal": {
"unicode": "1F918",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns",
"shortname": ":metal:",
"category": "people",
@@ -17242,12 +19342,19 @@
"band",
"concert",
"fingers",
- "rocknroll"
- ]
+ "rocknroll",
+ "body",
+ "hands",
+ "hi",
+ "diversity",
+ "boys night",
+ "parties"
+ ],
+ "moji": "🤘"
},
"metal_tone1": {
"unicode": "1F918-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns tone 1",
"shortname": ":metal_tone1:",
"category": "people",
@@ -17260,11 +19367,12 @@
"concert",
"fingers",
"rocknroll"
- ]
+ ],
+ "moji": "🤘🏻"
},
"metal_tone2": {
"unicode": "1F918-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns tone 2",
"shortname": ":metal_tone2:",
"category": "people",
@@ -17277,11 +19385,12 @@
"concert",
"fingers",
"rocknroll"
- ]
+ ],
+ "moji": "🤘🏼"
},
"metal_tone3": {
"unicode": "1F918-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns tone 3",
"shortname": ":metal_tone3:",
"category": "people",
@@ -17294,11 +19403,12 @@
"concert",
"fingers",
"rocknroll"
- ]
+ ],
+ "moji": "🤘🏽"
},
"metal_tone4": {
"unicode": "1F918-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns tone 4",
"shortname": ":metal_tone4:",
"category": "people",
@@ -17311,11 +19421,12 @@
"concert",
"fingers",
"rocknroll"
- ]
+ ],
+ "moji": "🤘🏾"
},
"metal_tone5": {
"unicode": "1F918-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sign of the horns tone 5",
"shortname": ":metal_tone5:",
"category": "people",
@@ -17328,14 +19439,15 @@
"concert",
"fingers",
"rocknroll"
- ]
+ ],
+ "moji": "🤘🏿"
},
"metro": {
"unicode": "1F687",
"unicode_alternates": [],
"name": "metro",
"shortname": ":metro:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17346,8 +19458,8 @@
"underground",
"metro",
"subway",
- "underground",
- "train"
+ "train",
+ "travel"
],
"moji": "🚇"
},
@@ -17356,7 +19468,7 @@
"unicode_alternates": [],
"name": "microphone",
"shortname": ":microphone:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17366,9 +19478,9 @@
"microphone",
"mic",
"audio",
- "sound",
"voice",
- "karaoke"
+ "karaoke",
+ "instruments"
],
"moji": "🎤"
},
@@ -17377,7 +19489,7 @@
"unicode_alternates": [],
"name": "studio microphone",
"shortname": ":microphone2:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":studio_microphone:"
],
@@ -17385,8 +19497,11 @@
"keywords": [
"mic",
"audio",
- "recording"
- ]
+ "recording",
+ "electronics",
+ "object"
+ ],
+ "moji": "🎙"
},
"microscope": {
"unicode": "1F52C",
@@ -17399,7 +19514,9 @@
"keywords": [
"experiment",
"laboratory",
- "zoomin"
+ "zoomin",
+ "object",
+ "science"
],
"moji": "🔬"
},
@@ -17414,12 +19531,17 @@
],
"aliases_ascii": [],
"keywords": [
- "fu"
- ]
+ "fu",
+ "body",
+ "hands",
+ "middle finger",
+ "diversity"
+ ],
+ "moji": "🖕"
},
"middle_finger_tone1": {
"unicode": "1F595-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "reversed hand with middle finger extended tone 1",
"shortname": ":middle_finger_tone1:",
"category": "people",
@@ -17429,11 +19551,12 @@
"aliases_ascii": [],
"keywords": [
"fu"
- ]
+ ],
+ "moji": "🖕🏻"
},
"middle_finger_tone2": {
"unicode": "1F595-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "reversed hand with middle finger extended tone 2",
"shortname": ":middle_finger_tone2:",
"category": "people",
@@ -17443,11 +19566,12 @@
"aliases_ascii": [],
"keywords": [
"fu"
- ]
+ ],
+ "moji": "🖕🏼"
},
"middle_finger_tone3": {
"unicode": "1F595-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "reversed hand with middle finger extended tone 3",
"shortname": ":middle_finger_tone3:",
"category": "people",
@@ -17457,11 +19581,12 @@
"aliases_ascii": [],
"keywords": [
"fu"
- ]
+ ],
+ "moji": "🖕🏽"
},
"middle_finger_tone4": {
"unicode": "1F595-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "reversed hand with middle finger extended tone 4",
"shortname": ":middle_finger_tone4:",
"category": "people",
@@ -17471,11 +19596,12 @@
"aliases_ascii": [],
"keywords": [
"fu"
- ]
+ ],
+ "moji": "🖕🏾"
},
"middle_finger_tone5": {
"unicode": "1F595-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "reversed hand with middle finger extended tone 5",
"shortname": ":middle_finger_tone5:",
"category": "people",
@@ -17485,14 +19611,15 @@
"aliases_ascii": [],
"keywords": [
"fu"
- ]
+ ],
+ "moji": "🖕🏿"
},
"military_medal": {
"unicode": "1F396",
"unicode_alternates": [],
"name": "military medal",
"shortname": ":military_medal:",
- "category": "celebration",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17500,15 +19627,32 @@
"acknowledgment",
"purple heart",
"heroism",
- "veteran"
- ]
+ "veteran",
+ "object",
+ "award",
+ "win"
+ ],
+ "moji": "🎖"
+ },
+ "milk": {
+ "unicode": "1F95B",
+ "unicode_alternates": [],
+ "name": "glass of milk",
+ "shortname": ":milk:",
+ "category": "food",
+ "aliases": [
+ ":glass_of_milk:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥛"
},
"milky_way": {
"unicode": "1F30C",
"unicode_alternates": [],
"name": "milky way",
"shortname": ":milky_way:",
- "category": "nature",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17519,8 +19663,10 @@
"star",
"stars",
"planets",
- "space",
- "sky"
+ "sky",
+ "places",
+ "travel",
+ "vacation"
],
"moji": "🌌"
},
@@ -17529,7 +19675,7 @@
"unicode_alternates": [],
"name": "minibus",
"shortname": ":minibus:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17538,8 +19684,7 @@
"vehicle",
"bus",
"city",
- "transport",
- "transportation"
+ "transport"
],
"moji": "🚐"
},
@@ -17556,7 +19701,8 @@
"disc",
"disk",
"record",
- "technology"
+ "technology",
+ "electronics"
],
"moji": "💽"
},
@@ -17565,17 +19711,18 @@
"unicode_alternates": [],
"name": "mobile phone off",
"shortname": ":mobile_phone_off:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "mute"
+ "mute",
+ "symbol"
],
"moji": "📴"
},
"money_mouth": {
"unicode": "1F911",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "money-mouth face",
"shortname": ":money_mouth:",
"category": "people",
@@ -17583,7 +19730,14 @@
":money_mouth_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "smiley",
+ "win",
+ "money",
+ "emotion",
+ "boys night"
+ ],
+ "moji": "🤑"
},
"money_with_wings": {
"unicode": "1F4B8",
@@ -17607,7 +19761,7 @@
"burned",
"gift",
"cash",
- "dollar"
+ "boys night"
],
"moji": "💸"
},
@@ -17622,7 +19776,10 @@
"keywords": [
"coins",
"dollar",
- "payment"
+ "payment",
+ "bag",
+ "award",
+ "money"
],
"moji": "💰"
},
@@ -17640,7 +19797,8 @@
"monkey",
"primate",
"banana",
- "silly"
+ "silly",
+ "wildlife"
],
"moji": "🐒"
},
@@ -17663,7 +19821,7 @@
"unicode_alternates": [],
"name": "monorail",
"shortname": ":monorail:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17672,66 +19830,18 @@
"train",
"mono",
"rail",
- "transport"
+ "transport",
+ "travel",
+ "vacation"
],
"moji": "🚝"
},
- "mood_bubble": {
- "unicode": "1F5F0",
- "unicode_alternates": [],
- "name": "mood bubble",
- "shortname": ":mood_bubble:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "conversation",
- "communication",
- "comic",
- "feeling"
- ]
- },
- "mood_bubble_lightning": {
- "unicode": "1F5F1",
- "unicode_alternates": [],
- "name": "lightning mood bubble",
- "shortname": ":mood_bubble_lightning:",
- "category": "objects_symbols",
- "aliases": [
- ":lightning_mood_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "conversation",
- "communication",
- "comic",
- "feeling"
- ]
- },
- "mood_lightning": {
- "unicode": "1F5F2",
- "unicode_alternates": [],
- "name": "lightning mood",
- "shortname": ":mood_lightning:",
- "category": "objects_symbols",
- "aliases": [
- ":lightning_mood:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "zap",
- "electric",
- "current"
- ]
- },
"mortar_board": {
"unicode": "1F393",
"unicode_alternates": [],
"name": "graduation cap",
"shortname": ":mortar_board:",
- "category": "objects",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17742,33 +19852,55 @@
"hat",
"school",
"university",
- "graduation",
- "cap",
"mortarboard",
"academic",
"education",
"ceremony",
"square",
- "tassel"
+ "tassel",
+ "office",
+ "accessories"
],
"moji": "🎓"
},
"mosque": {
"unicode": "1F54C",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mosque",
"shortname": ":mosque:",
"category": "travel",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "places",
+ "religion",
+ "building",
+ "vacation",
+ "condolence"
+ ],
+ "moji": "🕌"
+ },
+ "motor_scooter": {
+ "unicode": "1F6F5",
+ "unicode_alternates": [],
+ "name": "motor scooter",
+ "shortname": ":motor_scooter:",
+ "category": "travel",
+ "aliases": [
+ ":motorbike:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "moped"
+ ],
+ "moji": "🛵"
},
"motorboat": {
"unicode": "1F6E5",
"unicode_alternates": [],
"name": "motorboat",
"shortname": ":motorboat:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17776,30 +19908,35 @@
"vehicle",
"boat",
"speedboat",
- "powerboat"
- ]
+ "powerboat",
+ "travel"
+ ],
+ "moji": "🛥"
},
"motorcycle": {
"unicode": "1F3CD",
"unicode_alternates": [],
"name": "racing motorcycle",
"shortname": ":motorcycle:",
- "category": "activity",
+ "category": "travel",
"aliases": [
":racing_motorcycle:"
],
"aliases_ascii": [],
"keywords": [
"bike",
- "speed"
- ]
+ "speed",
+ "transportation",
+ "travel"
+ ],
+ "moji": "🏍"
},
"motorway": {
"unicode": "1F6E3",
"unicode_alternates": [],
"name": "motorway",
"shortname": ":motorway:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17807,43 +19944,56 @@
"highway",
"freeway",
"traffic",
- "travel"
- ]
+ "travel",
+ "vacation",
+ "camp"
+ ],
+ "moji": "🛣"
},
"mount_fuji": {
"unicode": "1F5FB",
"unicode_alternates": [],
"name": "mount fuji",
"shortname": ":mount_fuji:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"japan",
"mountain",
"nature",
- "photo"
+ "photo",
+ "places",
+ "travel",
+ "vacation",
+ "cold",
+ "camp"
],
"moji": "🗻"
},
"mountain": {
"unicode": "26F0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain",
"shortname": ":mountain:",
"category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "place"
- ]
+ "place",
+ "places",
+ "travel",
+ "vacation",
+ "camp"
+ ],
+ "moji": "⛰"
},
"mountain_bicyclist": {
"unicode": "1F6B5",
"unicode_alternates": [],
"name": "mountain bicyclist",
"shortname": ":mountain_bicyclist:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17855,13 +20005,15 @@
"bike",
"pedal",
"bicycle",
- "transportation"
+ "men",
+ "sport",
+ "diversity"
],
"moji": "🚵"
},
"mountain_bicyclist_tone1": {
"unicode": "1F6B5-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain bicyclist tone 1",
"shortname": ":mountain_bicyclist_tone1:",
"category": "activity",
@@ -17872,13 +20024,13 @@
"transportation",
"bike",
"pedal",
- "bicycle",
- "transportation"
- ]
+ "bicycle"
+ ],
+ "moji": "🚵🏻"
},
"mountain_bicyclist_tone2": {
"unicode": "1F6B5-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain bicyclist tone 2",
"shortname": ":mountain_bicyclist_tone2:",
"category": "activity",
@@ -17889,13 +20041,13 @@
"transportation",
"bike",
"pedal",
- "bicycle",
- "transportation"
- ]
+ "bicycle"
+ ],
+ "moji": "🚵🏼"
},
"mountain_bicyclist_tone3": {
"unicode": "1F6B5-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain bicyclist tone 3",
"shortname": ":mountain_bicyclist_tone3:",
"category": "activity",
@@ -17906,13 +20058,13 @@
"transportation",
"bike",
"pedal",
- "bicycle",
- "transportation"
- ]
+ "bicycle"
+ ],
+ "moji": "🚵🏽"
},
"mountain_bicyclist_tone4": {
"unicode": "1F6B5-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain bicyclist tone 4",
"shortname": ":mountain_bicyclist_tone4:",
"category": "activity",
@@ -17923,13 +20075,13 @@
"transportation",
"bike",
"pedal",
- "bicycle",
- "transportation"
- ]
+ "bicycle"
+ ],
+ "moji": "🚵🏾"
},
"mountain_bicyclist_tone5": {
"unicode": "1F6B5-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "mountain bicyclist tone 5",
"shortname": ":mountain_bicyclist_tone5:",
"category": "activity",
@@ -17940,16 +20092,16 @@
"transportation",
"bike",
"pedal",
- "bicycle",
- "transportation"
- ]
+ "bicycle"
+ ],
+ "moji": "🚵🏿"
},
"mountain_cableway": {
"unicode": "1F6A0",
"unicode_alternates": [],
"name": "mountain cableway",
"shortname": ":mountain_cableway:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17959,7 +20111,8 @@
"cable",
"rail",
"train",
- "railway"
+ "railway",
+ "travel"
],
"moji": "🚠"
},
@@ -17968,7 +20121,7 @@
"unicode_alternates": [],
"name": "mountain railway",
"shortname": ":mountain_railway:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -17977,7 +20130,8 @@
"railway",
"rail",
"train",
- "transport"
+ "transport",
+ "travel"
],
"moji": "🚞"
},
@@ -17986,7 +20140,7 @@
"unicode_alternates": [],
"name": "snow capped mountain",
"shortname": ":mountain_snow:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":snow_capped_mountain:"
],
@@ -17995,8 +20149,13 @@
"cold",
"elevation",
"hiking",
- "peak"
- ]
+ "peak",
+ "places",
+ "travel",
+ "vacation",
+ "camp"
+ ],
+ "moji": "🏔"
},
"mouse": {
"unicode": "1F42D",
@@ -18029,25 +20188,9 @@
],
"moji": "🐁"
},
- "mouse_one": {
- "unicode": "1F5AF",
- "unicode_alternates": [],
- "name": "one button mouse",
- "shortname": ":mouse_one:",
- "category": "objects_symbols",
- "aliases": [
- ":one_button_mouse:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "computer",
- "input",
- "device"
- ]
- },
"mouse_three_button": {
"unicode": "1F5B1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "three button mouse",
"shortname": ":mouse_three_button:",
"category": "objects",
@@ -18059,8 +20202,12 @@
"3",
"computer",
"object",
- "office"
- ]
+ "office",
+ "electronics",
+ "work",
+ "game"
+ ],
+ "moji": "🖱"
},
"movie_camera": {
"unicode": "1F3A5",
@@ -18078,7 +20225,8 @@
"camcorder",
"video",
"motion",
- "picture"
+ "picture",
+ "object"
],
"moji": "🎥"
},
@@ -18087,21 +20235,101 @@
"unicode_alternates": [],
"name": "moyai",
"shortname": ":moyai:",
- "category": "places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"island",
- "stone"
+ "stone",
+ "travel",
+ "vacation"
],
"moji": "🗿"
},
+ "mrs_claus": {
+ "unicode": "1F936",
+ "unicode_alternates": [],
+ "name": "mother christmas",
+ "shortname": ":mrs_claus:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶"
+ },
+ "mrs_claus_tone1": {
+ "unicode": "1F936-1F3FB",
+ "unicode_alternates": [],
+ "name": "mother christmas tone 1",
+ "shortname": ":mrs_claus_tone1:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶🏻"
+ },
+ "mrs_claus_tone2": {
+ "unicode": "1F936-1F3FC",
+ "unicode_alternates": [],
+ "name": "mother christmas tone 2",
+ "shortname": ":mrs_claus_tone2:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶🏼"
+ },
+ "mrs_claus_tone3": {
+ "unicode": "1F936-1F3FD",
+ "unicode_alternates": [],
+ "name": "mother christmas tone 3",
+ "shortname": ":mrs_claus_tone3:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶🏽"
+ },
+ "mrs_claus_tone4": {
+ "unicode": "1F936-1F3FE",
+ "unicode_alternates": [],
+ "name": "mother christmas tone 4",
+ "shortname": ":mrs_claus_tone4:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶🏾"
+ },
+ "mrs_claus_tone5": {
+ "unicode": "1F936-1F3FF",
+ "unicode_alternates": [],
+ "name": "mother christmas tone 5",
+ "shortname": ":mrs_claus_tone5:",
+ "category": "people",
+ "aliases": [
+ ":mother_christmas_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤶🏿"
+ },
"muscle": {
"unicode": "1F4AA",
"unicode_alternates": [],
"name": "flexed biceps",
"shortname": ":muscle:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18110,13 +20338,20 @@
"hand",
"strong",
"muscle",
- "bicep"
+ "bicep",
+ "body",
+ "hands",
+ "workout",
+ "win",
+ "diversity",
+ "feminist",
+ "boys night"
],
"moji": "💪"
},
"muscle_tone1": {
"unicode": "1F4AA-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "flexed biceps tone 1",
"shortname": ":muscle_tone1:",
"category": "people",
@@ -18129,11 +20364,12 @@
"strong",
"muscle",
"bicep"
- ]
+ ],
+ "moji": "💪🏻"
},
"muscle_tone2": {
"unicode": "1F4AA-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "flexed biceps tone 2",
"shortname": ":muscle_tone2:",
"category": "people",
@@ -18146,11 +20382,12 @@
"strong",
"muscle",
"bicep"
- ]
+ ],
+ "moji": "💪🏼"
},
"muscle_tone3": {
"unicode": "1F4AA-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "flexed biceps tone 3",
"shortname": ":muscle_tone3:",
"category": "people",
@@ -18163,11 +20400,12 @@
"strong",
"muscle",
"bicep"
- ]
+ ],
+ "moji": "💪🏽"
},
"muscle_tone4": {
"unicode": "1F4AA-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "flexed biceps tone 4",
"shortname": ":muscle_tone4:",
"category": "people",
@@ -18180,11 +20418,12 @@
"strong",
"muscle",
"bicep"
- ]
+ ],
+ "moji": "💪🏾"
},
"muscle_tone5": {
"unicode": "1F4AA-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "flexed biceps tone 5",
"shortname": ":muscle_tone5:",
"category": "people",
@@ -18197,7 +20436,8 @@
"strong",
"muscle",
"bicep"
- ]
+ ],
+ "moji": "💪🏿"
},
"mushroom": {
"unicode": "1F344",
@@ -18213,7 +20453,9 @@
"mushroom",
"fungi",
"food",
- "fungus"
+ "fungus",
+ "nature",
+ "drugs"
],
"moji": "🍄"
},
@@ -18222,7 +20464,7 @@
"unicode_alternates": [],
"name": "musical keyboard",
"shortname": ":musical_keyboard:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18230,10 +20472,9 @@
"piano",
"music",
"keyboard",
- "piano",
"organ",
- "instrument",
- "electric"
+ "electric",
+ "instruments"
],
"moji": "🎹"
},
@@ -18242,7 +20483,7 @@
"unicode_alternates": [],
"name": "musical note",
"shortname": ":musical_note:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18250,8 +20491,9 @@
"musical",
"music",
"note",
- "music",
- "sound"
+ "sound",
+ "instruments",
+ "symbol"
],
"moji": "🎵"
},
@@ -18260,7 +20502,7 @@
"unicode_alternates": [],
"name": "musical score",
"shortname": ":musical_score:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18269,10 +20511,10 @@
"music",
"musical",
"score",
- "clef",
"g-clef",
"stave",
- "staff"
+ "staff",
+ "instruments"
],
"moji": "🎼"
},
@@ -18281,12 +20523,14 @@
"unicode_alternates": [],
"name": "speaker with cancellation stroke",
"shortname": ":mute:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"sound",
- "volume"
+ "volume",
+ "alarm",
+ "symbol"
],
"moji": "🔇"
},
@@ -18295,18 +20539,24 @@
"unicode_alternates": [],
"name": "nail polish",
"shortname": ":nail_care:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"beauty",
- "manicure"
+ "manicure",
+ "women",
+ "body",
+ "hands",
+ "nailpolish",
+ "diversity",
+ "girls night"
],
"moji": "💅"
},
"nail_care_tone1": {
"unicode": "1F485-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nail polish tone 1",
"shortname": ":nail_care_tone1:",
"category": "people",
@@ -18315,11 +20565,12 @@
"keywords": [
"beauty",
"manicure"
- ]
+ ],
+ "moji": "💅🏻"
},
"nail_care_tone2": {
"unicode": "1F485-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nail polish tone 2",
"shortname": ":nail_care_tone2:",
"category": "people",
@@ -18328,11 +20579,12 @@
"keywords": [
"beauty",
"manicure"
- ]
+ ],
+ "moji": "💅🏼"
},
"nail_care_tone3": {
"unicode": "1F485-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nail polish tone 3",
"shortname": ":nail_care_tone3:",
"category": "people",
@@ -18341,11 +20593,12 @@
"keywords": [
"beauty",
"manicure"
- ]
+ ],
+ "moji": "💅🏽"
},
"nail_care_tone4": {
"unicode": "1F485-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nail polish tone 4",
"shortname": ":nail_care_tone4:",
"category": "people",
@@ -18354,11 +20607,12 @@
"keywords": [
"beauty",
"manicure"
- ]
+ ],
+ "moji": "💅🏾"
},
"nail_care_tone5": {
"unicode": "1F485-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nail polish tone 5",
"shortname": ":nail_care_tone5:",
"category": "people",
@@ -18367,28 +20621,43 @@
"keywords": [
"beauty",
"manicure"
- ]
+ ],
+ "moji": "💅🏿"
},
"name_badge": {
"unicode": "1F4DB",
"unicode_alternates": [],
"name": "name badge",
"shortname": ":name_badge:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fire",
- "forbid"
+ "forbid",
+ "work"
],
"moji": "📛"
},
+ "nauseated_face": {
+ "unicode": "1F922",
+ "unicode_alternates": [],
+ "name": "nauseated face",
+ "shortname": ":nauseated_face:",
+ "category": "people",
+ "aliases": [
+ ":sick:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤢"
+ },
"necktie": {
"unicode": "1F454",
"unicode_alternates": [],
"name": "necktie",
"shortname": ":necktie:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18405,20 +20674,21 @@
"unicode_alternates": [],
"name": "negative squared cross mark",
"shortname": ":negative_squared_cross_mark:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"deny",
"green-square",
"no",
- "x"
+ "x",
+ "symbol"
],
"moji": "❎"
},
"nerd": {
"unicode": "1F913",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nerd face",
"shortname": ":nerd:",
"category": "people",
@@ -18426,31 +20696,18 @@
":nerd_face:"
],
"aliases_ascii": [],
- "keywords": []
- },
- "network": {
- "unicode": "1F5A7",
- "unicode_alternates": [],
- "name": "three networked computers",
- "shortname": ":network:",
- "category": "objects_symbols",
- "aliases": [
- ":three_networked_computers:"
- ],
- "aliases_ascii": [],
"keywords": [
- "lan",
- "wan",
- "network",
- "technology"
- ]
+ "smiley",
+ "glasses"
+ ],
+ "moji": "🤓"
},
"neutral_face": {
"unicode": "1F610",
"unicode_alternates": [],
"name": "neutral face",
"shortname": ":neutral_face:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18459,7 +20716,11 @@
"neutral",
"objective",
"impartial",
- "blank"
+ "blank",
+ "mad",
+ "smiley",
+ "shrug",
+ "emotion"
],
"moji": "😐"
},
@@ -18468,11 +20729,12 @@
"unicode_alternates": [],
"name": "squared new",
"shortname": ":new:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "🆕"
},
@@ -18491,7 +20753,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌑"
},
@@ -18512,7 +20775,9 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space",
+ "goodnight"
],
"moji": "🌚"
},
@@ -18526,7 +20791,9 @@
"aliases_ascii": [],
"keywords": [
"headline",
- "press"
+ "press",
+ "office",
+ "write"
],
"moji": "📰"
},
@@ -18535,19 +20802,22 @@
"unicode_alternates": [],
"name": "rolled-up newspaper",
"shortname": ":newspaper2:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":rolled_up_newspaper:"
],
"aliases_ascii": [],
"keywords": [
"headline",
- "press"
- ]
+ "press",
+ "office",
+ "write"
+ ],
+ "moji": "🗞"
},
"ng": {
"unicode": "1F196",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "squared ng",
"shortname": ":ng:",
"category": "symbols",
@@ -18558,14 +20828,15 @@
"no good",
"symbol",
"word"
- ]
+ ],
+ "moji": "🆖"
},
"night_with_stars": {
"unicode": "1F303",
"unicode_alternates": [],
"name": "night with stars",
"shortname": ":night_with_stars:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18575,7 +20846,11 @@
"evening",
"planets",
"space",
- "sky"
+ "sky",
+ "places",
+ "building",
+ "vacation",
+ "goodnight"
],
"moji": "🌃"
},
@@ -18585,15 +20860,18 @@
"unicode_alternates": [
"0039-FE0F-20E3"
],
- "name": "digit nine",
+ "name": "keycap digit nine",
"shortname": ":nine:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"9",
"blue-square",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"no_bell": {
@@ -18601,13 +20879,15 @@
"unicode_alternates": [],
"name": "bell with cancellation stroke",
"shortname": ":no_bell:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"mute",
"sound",
- "volume"
+ "volume",
+ "alarm",
+ "symbol"
],
"moji": "🔕"
},
@@ -18616,7 +20896,7 @@
"unicode_alternates": [],
"name": "no bicycles",
"shortname": ":no_bicycles:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18624,7 +20904,8 @@
"prohibited",
"bicycle",
"bike pedal",
- "no"
+ "no",
+ "symbol"
],
"moji": "🚳"
},
@@ -18635,7 +20916,7 @@
],
"name": "no entry",
"shortname": ":no_entry:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18644,7 +20925,9 @@
"limit",
"privacy",
"security",
- "stop"
+ "stop",
+ "symbol",
+ "circle"
],
"moji": "⛔"
},
@@ -18653,7 +20936,7 @@
"unicode_alternates": [],
"name": "no entry sign",
"shortname": ":no_entry_sign:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18663,8 +20946,9 @@
"limit",
"stop",
"no",
- "stop",
- "entry"
+ "entry",
+ "symbol",
+ "circle"
],
"moji": "🚫"
},
@@ -18673,7 +20957,7 @@
"unicode_alternates": [],
"name": "face with no good gesture",
"shortname": ":no_good:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18684,13 +20968,17 @@
"stop",
"nope",
"don&#039;t",
- "not"
+ "not",
+ "people",
+ "women",
+ "diversity",
+ "girls night"
],
"moji": "🙅"
},
"no_good_tone1": {
"unicode": "1F645-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with no good gesture tone 1",
"shortname": ":no_good_tone1:",
"category": "people",
@@ -18708,11 +20996,12 @@
"hand",
"person",
"prohibited"
- ]
+ ],
+ "moji": "🙅🏻"
},
"no_good_tone2": {
"unicode": "1F645-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with no good gesture tone 2",
"shortname": ":no_good_tone2:",
"category": "people",
@@ -18730,11 +21019,12 @@
"hand",
"person",
"prohibited"
- ]
+ ],
+ "moji": "🙅🏼"
},
"no_good_tone3": {
"unicode": "1F645-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with no good gesture tone 3",
"shortname": ":no_good_tone3:",
"category": "people",
@@ -18752,11 +21042,12 @@
"hand",
"person",
"prohibited"
- ]
+ ],
+ "moji": "🙅🏽"
},
"no_good_tone4": {
"unicode": "1F645-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with no good gesture tone 4",
"shortname": ":no_good_tone4:",
"category": "people",
@@ -18774,11 +21065,12 @@
"hand",
"person",
"prohibited"
- ]
+ ],
+ "moji": "🙅🏾"
},
"no_good_tone5": {
"unicode": "1F645-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with no good gesture tone 5",
"shortname": ":no_good_tone5:",
"category": "people",
@@ -18796,19 +21088,22 @@
"hand",
"person",
"prohibited"
- ]
+ ],
+ "moji": "🙅🏿"
},
"no_mobile_phones": {
"unicode": "1F4F5",
"unicode_alternates": [],
"name": "no mobile phones",
"shortname": ":no_mobile_phones:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"iphone",
- "mute"
+ "mute",
+ "symbol",
+ "phone"
],
"moji": "📵"
},
@@ -18817,7 +21112,7 @@
"unicode_alternates": [],
"name": "face without mouth",
"shortname": ":no_mouth:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":-X",
@@ -18835,7 +21130,11 @@
"hellokitty",
"mouth",
"silent",
- "vapid"
+ "vapid",
+ "mad",
+ "smiley",
+ "neutral",
+ "emotion"
],
"moji": "😶"
},
@@ -18844,7 +21143,7 @@
"unicode_alternates": [],
"name": "no pedestrians",
"shortname": ":no_pedestrians:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18857,7 +21156,8 @@
"stroll",
"stride",
"foot",
- "feet"
+ "feet",
+ "symbol"
],
"moji": "🚷"
},
@@ -18866,20 +21166,20 @@
"unicode_alternates": [],
"name": "no smoking symbol",
"shortname": ":no_smoking:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cigarette",
"no",
"smoking",
- "cigarette",
"smoke",
"cancer",
"lungs",
"inhale",
"tar",
- "nicotine"
+ "nicotine",
+ "symbol"
],
"moji": "🚭"
},
@@ -18888,7 +21188,7 @@
"unicode_alternates": [],
"name": "non-potable water symbol",
"shortname": ":non-potable_water:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -18901,7 +21201,8 @@
"dirty",
"gross",
"aqua",
- "h20"
+ "h20",
+ "symbol"
],
"moji": "🚱"
},
@@ -18910,18 +21211,20 @@
"unicode_alternates": [],
"name": "nose",
"shortname": ":nose:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"smell",
- "sniff"
+ "sniff",
+ "body",
+ "diversity"
],
"moji": "👃"
},
"nose_tone1": {
"unicode": "1F443-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nose tone 1",
"shortname": ":nose_tone1:",
"category": "people",
@@ -18930,11 +21233,12 @@
"keywords": [
"smell",
"sniff"
- ]
+ ],
+ "moji": "👃🏻"
},
"nose_tone2": {
"unicode": "1F443-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nose tone 2",
"shortname": ":nose_tone2:",
"category": "people",
@@ -18943,11 +21247,12 @@
"keywords": [
"smell",
"sniff"
- ]
+ ],
+ "moji": "👃🏼"
},
"nose_tone3": {
"unicode": "1F443-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nose tone 3",
"shortname": ":nose_tone3:",
"category": "people",
@@ -18956,11 +21261,12 @@
"keywords": [
"smell",
"sniff"
- ]
+ ],
+ "moji": "👃🏽"
},
"nose_tone4": {
"unicode": "1F443-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nose tone 4",
"shortname": ":nose_tone4:",
"category": "people",
@@ -18969,11 +21275,12 @@
"keywords": [
"smell",
"sniff"
- ]
+ ],
+ "moji": "👃🏾"
},
"nose_tone5": {
"unicode": "1F443-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "nose tone 5",
"shortname": ":nose_tone5:",
"category": "people",
@@ -18982,37 +21289,8 @@
"keywords": [
"smell",
"sniff"
- ]
- },
- "note": {
- "unicode": "1F5C9",
- "unicode_alternates": [],
- "name": "note page",
- "shortname": ":note:",
- "category": "objects_symbols",
- "aliases": [
- ":note_page:"
],
- "aliases_ascii": [],
- "keywords": [
- "stationery",
- "post-it"
- ]
- },
- "note_empty": {
- "unicode": "1F5C6",
- "unicode_alternates": [],
- "name": "empty note page",
- "shortname": ":note_empty:",
- "category": "objects_symbols",
- "aliases": [
- ":empty_note_page:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "stationery",
- "post-it"
- ]
+ "moji": "👃🏿"
},
"notebook": {
"unicode": "1F4D3",
@@ -19026,7 +21304,10 @@
"notes",
"paper",
"record",
- "stationery"
+ "stationery",
+ "object",
+ "office",
+ "write"
],
"moji": "📓"
},
@@ -19042,71 +21323,48 @@
"classroom",
"notes",
"paper",
- "record"
+ "record",
+ "object",
+ "office",
+ "write"
],
"moji": "📔"
},
- "notepad": {
- "unicode": "1F5CA",
- "unicode_alternates": [],
- "name": "note pad",
- "shortname": ":notepad:",
- "category": "objects_symbols",
- "aliases": [
- ":note_pad:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "stationery",
- "post-it"
- ]
- },
- "notepad_empty": {
- "unicode": "1F5C7",
- "unicode_alternates": [],
- "name": "empty note pad",
- "shortname": ":notepad_empty:",
- "category": "objects_symbols",
- "aliases": [
- ":empty_note_pad:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "stationery",
- "post-it"
- ]
- },
"notepad_spiral": {
"unicode": "1F5D2",
"unicode_alternates": [],
"name": "spiral note pad",
"shortname": ":notepad_spiral:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":spiral_note_pad:"
],
"aliases_ascii": [],
"keywords": [
- "stationery"
- ]
+ "stationery",
+ "work",
+ "office",
+ "write"
+ ],
+ "moji": "🗒"
},
"notes": {
"unicode": "1F3B6",
"unicode_alternates": [],
"name": "multiple musical notes",
"shortname": ":notes:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"music",
"score",
"musical",
- "music",
"notes",
- "music",
"sound",
- "melody"
+ "melody",
+ "instruments",
+ "symbol"
],
"moji": "🎶"
},
@@ -19120,7 +21378,10 @@
"aliases_ascii": [],
"keywords": [
"handy",
- "tools"
+ "tools",
+ "object",
+ "tool",
+ "nutcase"
],
"moji": "🔩"
},
@@ -19131,12 +21392,13 @@
],
"name": "heavy large circle",
"shortname": ":o:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"circle",
- "round"
+ "round",
+ "symbol"
],
"moji": "⭕"
},
@@ -19145,13 +21407,14 @@
"unicode_alternates": [],
"name": "negative squared latin capital letter o",
"shortname": ":o2:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"letter",
- "red-square"
+ "red-square",
+ "symbol"
],
"moji": "🅾"
},
@@ -19168,13 +21431,29 @@
"water",
"wave",
"ocean",
- "wave",
"surf",
"beach",
- "tide"
+ "tide",
+ "weather",
+ "boat",
+ "tropical",
+ "swim"
],
"moji": "🌊"
},
+ "octagonal_sign": {
+ "unicode": "1F6D1",
+ "unicode_alternates": [],
+ "name": "octagonal sign",
+ "shortname": ":octagonal_sign:",
+ "category": "symbols",
+ "aliases": [
+ ":stop_sign:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🛑"
+ },
"octopus": {
"unicode": "1F419",
"unicode_alternates": [],
@@ -19187,7 +21466,8 @@
"animal",
"creature",
"ocean",
- "sea"
+ "sea",
+ "wildlife"
],
"moji": "🐙"
},
@@ -19196,7 +21476,7 @@
"unicode_alternates": [],
"name": "oden",
"shortname": ":oden:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19214,13 +21494,14 @@
"unicode_alternates": [],
"name": "office building",
"shortname": ":office:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"building",
"bureau",
- "work"
+ "work",
+ "places"
],
"moji": "🏢"
},
@@ -19229,28 +21510,31 @@
"unicode_alternates": [],
"name": "oil drum",
"shortname": ":oil:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":oil_drum:"
],
"aliases_ascii": [],
"keywords": [
- "petroleum"
- ]
+ "petroleum",
+ "object"
+ ],
+ "moji": "🛢"
},
"ok": {
"unicode": "1F197",
"unicode_alternates": [],
"name": "squared ok",
"shortname": ":ok:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"agree",
"blue-square",
"good",
- "yes"
+ "yes",
+ "symbol"
],
"moji": "🆗"
},
@@ -19259,7 +21543,7 @@
"unicode_alternates": [],
"name": "ok hand sign",
"shortname": ":ok_hand:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19273,13 +21557,19 @@
"marijuana",
"joint",
"pot",
- "420"
+ "420",
+ "body",
+ "hands",
+ "hi",
+ "diversity",
+ "good",
+ "beautiful"
],
"moji": "👌"
},
"ok_hand_tone1": {
"unicode": "1F44C-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ok hand sign tone 1",
"shortname": ":ok_hand_tone1:",
"category": "people",
@@ -19296,11 +21586,12 @@
"joint",
"pot",
"420"
- ]
+ ],
+ "moji": "👌🏻"
},
"ok_hand_tone2": {
"unicode": "1F44C-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ok hand sign tone 2",
"shortname": ":ok_hand_tone2:",
"category": "people",
@@ -19317,11 +21608,12 @@
"joint",
"pot",
"420"
- ]
+ ],
+ "moji": "👌🏼"
},
"ok_hand_tone3": {
"unicode": "1F44C-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ok hand sign tone 3",
"shortname": ":ok_hand_tone3:",
"category": "people",
@@ -19338,11 +21630,12 @@
"joint",
"pot",
"420"
- ]
+ ],
+ "moji": "👌🏽"
},
"ok_hand_tone4": {
"unicode": "1F44C-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ok hand sign tone 4",
"shortname": ":ok_hand_tone4:",
"category": "people",
@@ -19359,11 +21652,12 @@
"joint",
"pot",
"420"
- ]
+ ],
+ "moji": "👌🏾"
},
"ok_hand_tone5": {
"unicode": "1F44C-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "ok hand sign tone 5",
"shortname": ":ok_hand_tone5:",
"category": "people",
@@ -19380,14 +21674,15 @@
"joint",
"pot",
"420"
- ]
+ ],
+ "moji": "👌🏿"
},
"ok_woman": {
"unicode": "1F646",
"unicode_alternates": [],
"name": "face with ok gesture",
"shortname": ":ok_woman:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"*\\0/*",
@@ -19404,13 +21699,15 @@
"yes",
"ok",
"okay",
- "accept"
+ "accept",
+ "people",
+ "diversity"
],
"moji": "🙆"
},
"ok_woman_tone1": {
"unicode": "1F646-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with ok gesture tone1",
"shortname": ":ok_woman_tone1:",
"category": "people",
@@ -19425,11 +21722,12 @@
"yes",
"okay",
"accept"
- ]
+ ],
+ "moji": "🙆🏻"
},
"ok_woman_tone2": {
"unicode": "1F646-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with ok gesture tone2",
"shortname": ":ok_woman_tone2:",
"category": "people",
@@ -19444,11 +21742,12 @@
"yes",
"okay",
"accept"
- ]
+ ],
+ "moji": "🙆🏼"
},
"ok_woman_tone3": {
"unicode": "1F646-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with ok gesture tone3",
"shortname": ":ok_woman_tone3:",
"category": "people",
@@ -19463,11 +21762,12 @@
"yes",
"okay",
"accept"
- ]
+ ],
+ "moji": "🙆🏽"
},
"ok_woman_tone4": {
"unicode": "1F646-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with ok gesture tone4",
"shortname": ":ok_woman_tone4:",
"category": "people",
@@ -19482,11 +21782,12 @@
"yes",
"okay",
"accept"
- ]
+ ],
+ "moji": "🙆🏾"
},
"ok_woman_tone5": {
"unicode": "1F646-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with ok gesture tone5",
"shortname": ":ok_woman_tone5:",
"category": "people",
@@ -19501,26 +21802,30 @@
"yes",
"okay",
"accept"
- ]
+ ],
+ "moji": "🙆🏿"
},
"older_man": {
"unicode": "1F474",
"unicode_alternates": [],
"name": "older man",
"shortname": ":older_man:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"human",
"male",
- "men"
+ "men",
+ "people",
+ "old people",
+ "diversity"
],
"moji": "👴"
},
"older_man_tone1": {
"unicode": "1F474-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older man tone 1",
"shortname": ":older_man_tone1:",
"category": "people",
@@ -19531,11 +21836,12 @@
"men",
"grandpa",
"grandfather"
- ]
+ ],
+ "moji": "👴🏻"
},
"older_man_tone2": {
"unicode": "1F474-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older man tone 2",
"shortname": ":older_man_tone2:",
"category": "people",
@@ -19546,11 +21852,12 @@
"men",
"grandpa",
"grandfather"
- ]
+ ],
+ "moji": "👴🏼"
},
"older_man_tone3": {
"unicode": "1F474-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older man tone 3",
"shortname": ":older_man_tone3:",
"category": "people",
@@ -19561,11 +21868,12 @@
"men",
"grandpa",
"grandfather"
- ]
+ ],
+ "moji": "👴🏽"
},
"older_man_tone4": {
"unicode": "1F474-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older man tone 4",
"shortname": ":older_man_tone4:",
"category": "people",
@@ -19576,11 +21884,12 @@
"men",
"grandpa",
"grandfather"
- ]
+ ],
+ "moji": "👴🏾"
},
"older_man_tone5": {
"unicode": "1F474-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older man tone 5",
"shortname": ":older_man_tone5:",
"category": "people",
@@ -19591,14 +21900,15 @@
"men",
"grandpa",
"grandfather"
- ]
+ ],
+ "moji": "👴🏿"
},
"older_woman": {
"unicode": "1F475",
"unicode_alternates": [],
"name": "older woman",
"shortname": ":older_woman:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":grandma:"
],
@@ -19608,13 +21918,16 @@
"girl",
"women",
"grandma",
- "grandmother"
+ "grandmother",
+ "people",
+ "old people",
+ "diversity"
],
"moji": "👵"
},
"older_woman_tone1": {
"unicode": "1F475-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older woman tone 1",
"shortname": ":older_woman_tone1:",
"category": "people",
@@ -19628,11 +21941,12 @@
"lady",
"grandma",
"grandmother"
- ]
+ ],
+ "moji": "👵🏻"
},
"older_woman_tone2": {
"unicode": "1F475-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older woman tone 2",
"shortname": ":older_woman_tone2:",
"category": "people",
@@ -19646,11 +21960,12 @@
"lady",
"grandma",
"grandmother"
- ]
+ ],
+ "moji": "👵🏼"
},
"older_woman_tone3": {
"unicode": "1F475-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older woman tone 3",
"shortname": ":older_woman_tone3:",
"category": "people",
@@ -19664,11 +21979,12 @@
"lady",
"grandma",
"grandmother"
- ]
+ ],
+ "moji": "👵🏽"
},
"older_woman_tone4": {
"unicode": "1F475-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older woman tone 4",
"shortname": ":older_woman_tone4:",
"category": "people",
@@ -19682,11 +21998,12 @@
"lady",
"grandma",
"grandmother"
- ]
+ ],
+ "moji": "👵🏾"
},
"older_woman_tone5": {
"unicode": "1F475-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "older woman tone 5",
"shortname": ":older_woman_tone5:",
"category": "people",
@@ -19700,14 +22017,15 @@
"lady",
"grandma",
"grandmother"
- ]
+ ],
+ "moji": "👵🏿"
},
"om_symbol": {
"unicode": "1F549",
"unicode_alternates": [],
"name": "om symbol",
"shortname": ":om_symbol:",
- "category": "objects_symbols",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19718,20 +22036,24 @@
"dharmic",
"buddhism",
"jainism",
- "meditate"
- ]
+ "meditate",
+ "religion",
+ "symbol"
+ ],
+ "moji": "🕉"
},
"on": {
"unicode": "1F51B",
"unicode_alternates": [],
"name": "on with exclamation mark with left right arrow abo",
"shortname": ":on:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "words"
+ "words",
+ "symbol"
],
"moji": "🔛"
},
@@ -19740,7 +22062,7 @@
"unicode_alternates": [],
"name": "oncoming automobile",
"shortname": ":oncoming_automobile:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19748,8 +22070,8 @@
"transportation",
"vehicle",
"sedan",
- "car",
- "automobile"
+ "automobile",
+ "travel"
],
"moji": "🚘"
},
@@ -19758,7 +22080,7 @@
"unicode_alternates": [],
"name": "oncoming bus",
"shortname": ":oncoming_bus:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19767,8 +22089,8 @@
"bus",
"school",
"city",
- "transportation",
- "public"
+ "public",
+ "travel"
],
"moji": "🚍"
},
@@ -19777,7 +22099,7 @@
"unicode_alternates": [],
"name": "oncoming police car",
"shortname": ":oncoming_police_car:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19791,7 +22113,9 @@
"citation",
"crime",
"help",
- "officer"
+ "officer",
+ "transportation",
+ "911"
],
"moji": "🚔"
},
@@ -19800,7 +22124,7 @@
"unicode_alternates": [],
"name": "oncoming taxi",
"shortname": ":oncoming_taxi:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19812,7 +22136,9 @@
"automobile",
"city",
"transport",
- "service"
+ "service",
+ "transportation",
+ "travel"
],
"moji": "🚖"
},
@@ -19822,15 +22148,18 @@
"unicode_alternates": [
"0031-FE0F-20E3"
],
- "name": "digit one",
+ "name": "keycap digit one",
"shortname": ":one:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"1",
"blue-square",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"open_file_folder": {
@@ -19843,7 +22172,9 @@
"aliases_ascii": [],
"keywords": [
"documents",
- "load"
+ "load",
+ "work",
+ "office"
],
"moji": "📂"
},
@@ -19852,18 +22183,22 @@
"unicode_alternates": [],
"name": "open hands sign",
"shortname": ":open_hands:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"butterfly",
- "fingers"
+ "fingers",
+ "body",
+ "hands",
+ "diversity",
+ "condolence"
],
"moji": "👐"
},
"open_hands_tone1": {
"unicode": "1F450-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "open hands sign tone 1",
"shortname": ":open_hands_tone1:",
"category": "people",
@@ -19872,11 +22207,12 @@
"keywords": [
"butterfly",
"fingers"
- ]
+ ],
+ "moji": "👐🏻"
},
"open_hands_tone2": {
"unicode": "1F450-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "open hands sign tone 2",
"shortname": ":open_hands_tone2:",
"category": "people",
@@ -19885,11 +22221,12 @@
"keywords": [
"butterfly",
"fingers"
- ]
+ ],
+ "moji": "👐🏼"
},
"open_hands_tone3": {
"unicode": "1F450-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "open hands sign tone 3",
"shortname": ":open_hands_tone3:",
"category": "people",
@@ -19898,11 +22235,12 @@
"keywords": [
"butterfly",
"fingers"
- ]
+ ],
+ "moji": "👐🏽"
},
"open_hands_tone4": {
"unicode": "1F450-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "open hands sign tone 4",
"shortname": ":open_hands_tone4:",
"category": "people",
@@ -19911,11 +22249,12 @@
"keywords": [
"butterfly",
"fingers"
- ]
+ ],
+ "moji": "👐🏾"
},
"open_hands_tone5": {
"unicode": "1F450-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "open hands sign tone 5",
"shortname": ":open_hands_tone5:",
"category": "people",
@@ -19924,14 +22263,15 @@
"keywords": [
"butterfly",
"fingers"
- ]
+ ],
+ "moji": "👐🏿"
},
"open_mouth": {
"unicode": "1F62E",
"unicode_alternates": [],
"name": "face with open mouth",
"shortname": ":open_mouth:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":-O",
@@ -19949,7 +22289,10 @@
"jaw",
"gapping",
"surprise",
- "wow"
+ "wow",
+ "smiley",
+ "surprised",
+ "emotion"
],
"moji": "😮"
},
@@ -19958,7 +22301,7 @@
"unicode_alternates": [],
"name": "ophiuchus",
"shortname": ":ophiuchus:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -19972,28 +22315,11 @@
"zodiac",
"purple-square",
"sign",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "⛎"
},
- "optical_disk": {
- "unicode": "1F5B8",
- "unicode_alternates": [],
- "name": "optical disc icon",
- "shortname": ":optical_disk:",
- "category": "objects_symbols",
- "aliases": [
- ":optical_disc_icon:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "cd",
- "dvd",
- "disc",
- "disk",
- "technology"
- ]
- },
"orange_book": {
"unicode": "1F4D9",
"unicode_alternates": [],
@@ -20005,13 +22331,17 @@
"keywords": [
"knowledge",
"library",
- "read"
+ "read",
+ "object",
+ "office",
+ "write",
+ "book"
],
"moji": "📙"
},
"orthodox_cross": {
"unicode": "2626",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "orthodox cross",
"shortname": ":orthodox_cross:",
"category": "symbols",
@@ -20021,7 +22351,8 @@
"christian",
"religion",
"symbol"
- ]
+ ],
+ "moji": "☦"
},
"outbox_tray": {
"unicode": "1F4E4",
@@ -20033,10 +22364,23 @@
"aliases_ascii": [],
"keywords": [
"email",
- "inbox"
+ "inbox",
+ "work",
+ "office"
],
"moji": "📤"
},
+ "owl": {
+ "unicode": "1F989",
+ "unicode_alternates": [],
+ "name": "owl",
+ "shortname": ":owl:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦉"
+ },
"ox": {
"unicode": "1F402",
"unicode_alternates": [],
@@ -20062,22 +22406,12 @@
"aliases_ascii": [],
"keywords": [
"gift",
- "mail"
+ "mail",
+ "object",
+ "office"
],
"moji": "📦"
},
- "page": {
- "unicode": "1F5CF",
- "unicode_alternates": [],
- "name": "page",
- "shortname": ":page:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "document"
- ]
- },
"page_facing_up": {
"unicode": "1F4C4",
"unicode_alternates": [],
@@ -20087,7 +22421,10 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "documents"
+ "documents",
+ "work",
+ "office",
+ "write"
],
"moji": "📄"
},
@@ -20100,7 +22437,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "documents"
+ "documents",
+ "office",
+ "write"
],
"moji": "📃"
},
@@ -20114,28 +22453,18 @@
"aliases_ascii": [],
"keywords": [
"bbcall",
- "oldschool"
+ "oldschool",
+ "electronics",
+ "work"
],
"moji": "📟"
},
- "pages": {
- "unicode": "1F5D0",
- "unicode_alternates": [],
- "name": "pages",
- "shortname": ":pages:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "documents"
- ]
- },
"paintbrush": {
"unicode": "1F58C",
"unicode_alternates": [],
"name": "lower left paintbrush",
"shortname": ":paintbrush:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":lower_left_paintbrush:"
],
@@ -20143,8 +22472,12 @@
"keywords": [
"brush",
"art",
- "painting"
- ]
+ "painting",
+ "object",
+ "office",
+ "write"
+ ],
+ "moji": "🖌"
},
"palm_tree": {
"unicode": "1F334",
@@ -20163,10 +22496,22 @@
"coconuts",
"fronds",
"warm",
- "tropical"
+ "tropical",
+ "trees"
],
"moji": "🌴"
},
+ "pancakes": {
+ "unicode": "1F95E",
+ "unicode_alternates": [],
+ "name": "pancakes",
+ "shortname": ":pancakes:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥞"
+ },
"panda_face": {
"unicode": "1F43C",
"unicode_alternates": [],
@@ -20189,7 +22534,9 @@
"bamboo",
"china",
"black",
- "white"
+ "white",
+ "wildlife",
+ "roar"
],
"moji": "🐼"
},
@@ -20203,7 +22550,10 @@
"aliases_ascii": [],
"keywords": [
"documents",
- "stationery"
+ "stationery",
+ "object",
+ "work",
+ "office"
],
"moji": "📎"
},
@@ -20212,22 +22562,26 @@
"unicode_alternates": [],
"name": "linked paperclips",
"shortname": ":paperclips:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":linked_paperclips:"
],
"aliases_ascii": [],
"keywords": [
"documents",
- "stationery"
- ]
+ "stationery",
+ "object",
+ "work",
+ "office"
+ ],
+ "moji": "🖇"
},
"park": {
"unicode": "1F3DE",
"unicode_alternates": [],
"name": "national park",
"shortname": ":park:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":national_park:"
],
@@ -20238,8 +22592,13 @@
"wildlife",
"forest",
"wilderness",
- "national"
- ]
+ "national",
+ "travel",
+ "vacation",
+ "park",
+ "camp"
+ ],
+ "moji": "🏞"
},
"parking": {
"unicode": "1F17F",
@@ -20248,14 +22607,15 @@
],
"name": "negative squared latin capital letter p",
"shortname": ":parking:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
"blue-square",
"cars",
- "letter"
+ "letter",
+ "symbol"
],
"moji": "🅿"
},
@@ -20266,7 +22626,7 @@
],
"name": "part alternation mark",
"shortname": ":part_alternation_mark:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20279,7 +22639,8 @@
"cue",
"letter",
"m",
- "japanese"
+ "japanese",
+ "symbol"
],
"moji": "〽"
},
@@ -20297,7 +22658,9 @@
"cloud",
"morning",
"nature",
- "weather"
+ "weather",
+ "sky",
+ "sun"
],
"moji": "⛅"
},
@@ -20306,7 +22669,7 @@
"unicode_alternates": [],
"name": "passport control",
"shortname": ":passport_control:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20317,13 +22680,14 @@
"travel",
"control",
"foreign",
- "identification"
+ "identification",
+ "symbol"
],
"moji": "🛂"
},
"pause_button": {
"unicode": "23F8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "double vertical bar",
"shortname": ":pause_button:",
"category": "symbols",
@@ -20335,11 +22699,12 @@
"pause",
"sound",
"symbol"
- ]
+ ],
+ "moji": "⏸"
},
"peace": {
"unicode": "262E",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "peace symbol",
"shortname": ":peace:",
"category": "symbols",
@@ -20348,15 +22713,19 @@
],
"aliases_ascii": [],
"keywords": [
- "sign"
- ]
+ "sign",
+ "symbol",
+ "peace",
+ "drugs"
+ ],
+ "moji": "☮"
},
"peach": {
"unicode": "1F351",
"unicode_alternates": [],
"name": "peach",
"shortname": ":peach:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20364,26 +22733,39 @@
"fruit",
"nature",
"peach",
- "fruit",
"juicy",
- "pit"
+ "pit",
+ "butt"
],
"moji": "🍑"
},
+ "peanuts": {
+ "unicode": "1F95C",
+ "unicode_alternates": [],
+ "name": "peanuts",
+ "shortname": ":peanuts:",
+ "category": "food",
+ "aliases": [
+ ":shelled_peanut:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥜"
+ },
"pear": {
"unicode": "1F350",
"unicode_alternates": [],
"name": "pear",
"shortname": ":pear:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fruit",
"nature",
"pear",
- "fruit",
- "shape"
+ "shape",
+ "food"
],
"moji": "🍐"
},
@@ -20392,7 +22774,7 @@
"unicode_alternates": [],
"name": "lower left ballpoint pen",
"shortname": ":pen_ballpoint:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":lower_left_ballpoint_pen:"
],
@@ -20400,15 +22782,18 @@
"keywords": [
"write",
"bic",
- "ink"
- ]
+ "ink",
+ "object",
+ "office"
+ ],
+ "moji": "🖊"
},
"pen_fountain": {
"unicode": "1F58B",
"unicode_alternates": [],
"name": "lower left fountain pen",
"shortname": ":pen_fountain:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":lower_left_fountain_pen:"
],
@@ -20416,8 +22801,11 @@
"keywords": [
"write",
"calligraphy",
- "ink"
- ]
+ "ink",
+ "object",
+ "office"
+ ],
+ "moji": "🖋"
},
"pencil": {
"unicode": "1F4DD",
@@ -20433,7 +22821,9 @@
"documents",
"paper",
"station",
- "write"
+ "write",
+ "work",
+ "office"
],
"moji": "📝"
},
@@ -20450,26 +22840,12 @@
"keywords": [
"paper",
"stationery",
- "write"
+ "write",
+ "object",
+ "office"
],
"moji": "✏"
},
- "pencil3": {
- "unicode": "1F589",
- "unicode_alternates": [],
- "name": "lower left pencil",
- "shortname": ":pencil3:",
- "category": "objects_symbols",
- "aliases": [
- ":lower_left_pencil:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "paper",
- "stationery",
- "write"
- ]
- },
"penguin": {
"unicode": "1F427",
"unicode_alternates": [],
@@ -20480,46 +22856,17 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐧"
},
- "pennant_black": {
- "unicode": "1F3F2",
- "unicode_alternates": [],
- "name": "black pennant",
- "shortname": ":pennant_black:",
- "category": "objects_symbols",
- "aliases": [
- ":black_pennant:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "flag",
- "athletics"
- ]
- },
- "pennant_white": {
- "unicode": "1F3F1",
- "unicode_alternates": [],
- "name": "white pennant",
- "shortname": ":pennant_white:",
- "category": "objects_symbols",
- "aliases": [
- ":white_pennant:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "flag",
- "athletics"
- ]
- },
"pensive": {
"unicode": "1F614",
"unicode_alternates": [],
"name": "pensive face",
"shortname": ":pensive:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20532,7 +22879,10 @@
"reflective",
"wistful",
"meditate",
- "serious"
+ "serious",
+ "smiley",
+ "emotion",
+ "rip"
],
"moji": "😔"
},
@@ -20541,7 +22891,7 @@
"unicode_alternates": [],
"name": "performing arts",
"shortname": ":performing_arts:",
- "category": "places",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20552,10 +22902,11 @@
"arts",
"performance",
"entertainment",
- "acting",
"story",
"mask",
- "masks"
+ "masks",
+ "theatre",
+ "movie"
],
"moji": "🎭"
},
@@ -20564,7 +22915,7 @@
"unicode_alternates": [],
"name": "persevering face",
"shortname": ":persevere:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
">.<"
@@ -20575,7 +22926,11 @@
"face",
"no",
"sick",
- "upset"
+ "upset",
+ "sad",
+ "smiley",
+ "angry",
+ "emotion"
],
"moji": "😣"
},
@@ -20584,7 +22939,7 @@
"unicode_alternates": [],
"name": "person frowning",
"shortname": ":person_frowning:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20594,13 +22949,16 @@
"dejected",
"rejected",
"sad",
- "frown"
+ "frown",
+ "people",
+ "women",
+ "diversity"
],
"moji": "🙍"
},
"person_frowning_tone1": {
"unicode": "1F64D-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person frowning tone 1",
"shortname": ":person_frowning_tone1:",
"category": "people",
@@ -20614,11 +22972,12 @@
"rejected",
"sad",
"frown"
- ]
+ ],
+ "moji": "🙍🏻"
},
"person_frowning_tone2": {
"unicode": "1F64D-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person frowning tone 2",
"shortname": ":person_frowning_tone2:",
"category": "people",
@@ -20632,11 +22991,12 @@
"rejected",
"sad",
"frown"
- ]
+ ],
+ "moji": "🙍🏼"
},
"person_frowning_tone3": {
"unicode": "1F64D-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person frowning tone 3",
"shortname": ":person_frowning_tone3:",
"category": "people",
@@ -20650,11 +23010,12 @@
"rejected",
"sad",
"frown"
- ]
+ ],
+ "moji": "🙍🏽"
},
"person_frowning_tone4": {
"unicode": "1F64D-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person frowning tone 4",
"shortname": ":person_frowning_tone4:",
"category": "people",
@@ -20668,11 +23029,12 @@
"rejected",
"sad",
"frown"
- ]
+ ],
+ "moji": "🙍🏾"
},
"person_frowning_tone5": {
"unicode": "1F64D-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person frowning tone 5",
"shortname": ":person_frowning_tone5:",
"category": "people",
@@ -20686,14 +23048,15 @@
"rejected",
"sad",
"frown"
- ]
+ ],
+ "moji": "🙍🏿"
},
"person_with_blond_hair": {
"unicode": "1F471",
"unicode_alternates": [],
"name": "person with blond hair",
"shortname": ":person_with_blond_hair:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20703,13 +23066,16 @@
"young",
"western",
"westerner",
- "occidental"
+ "occidental",
+ "people",
+ "men",
+ "diversity"
],
"moji": "👱"
},
"person_with_blond_hair_tone1": {
"unicode": "1F471-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with blond hair tone 1",
"shortname": ":person_with_blond_hair_tone1:",
"category": "people",
@@ -20723,11 +23089,12 @@
"western",
"westerner",
"occidental"
- ]
+ ],
+ "moji": "👱🏻"
},
"person_with_blond_hair_tone2": {
"unicode": "1F471-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with blond hair tone 2",
"shortname": ":person_with_blond_hair_tone2:",
"category": "people",
@@ -20741,11 +23108,12 @@
"western",
"westerner",
"occidental"
- ]
+ ],
+ "moji": "👱🏼"
},
"person_with_blond_hair_tone3": {
"unicode": "1F471-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with blond hair tone 3",
"shortname": ":person_with_blond_hair_tone3:",
"category": "people",
@@ -20759,11 +23127,12 @@
"western",
"westerner",
"occidental"
- ]
+ ],
+ "moji": "👱🏽"
},
"person_with_blond_hair_tone4": {
"unicode": "1F471-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with blond hair tone 4",
"shortname": ":person_with_blond_hair_tone4:",
"category": "people",
@@ -20777,11 +23146,12 @@
"western",
"westerner",
"occidental"
- ]
+ ],
+ "moji": "👱🏾"
},
"person_with_blond_hair_tone5": {
"unicode": "1F471-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with blond hair tone 5",
"shortname": ":person_with_blond_hair_tone5:",
"category": "people",
@@ -20795,14 +23165,15 @@
"western",
"westerner",
"occidental"
- ]
+ ],
+ "moji": "👱🏿"
},
"person_with_pouting_face": {
"unicode": "1F64E",
"unicode_alternates": [],
"name": "person with pouting face",
"shortname": ":person_with_pouting_face:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -20812,13 +23183,16 @@
"pout",
"sexy",
"cute",
- "annoyed"
+ "annoyed",
+ "people",
+ "women",
+ "diversity"
],
"moji": "🙎"
},
"person_with_pouting_face_tone1": {
"unicode": "1F64E-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with pouting face tone1",
"shortname": ":person_with_pouting_face_tone1:",
"category": "people",
@@ -20832,11 +23206,12 @@
"sexy",
"cute",
"annoyed"
- ]
+ ],
+ "moji": "🙎🏻"
},
"person_with_pouting_face_tone2": {
"unicode": "1F64E-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with pouting face tone2",
"shortname": ":person_with_pouting_face_tone2:",
"category": "people",
@@ -20850,11 +23225,12 @@
"sexy",
"cute",
"annoyed"
- ]
+ ],
+ "moji": "🙎🏼"
},
"person_with_pouting_face_tone3": {
"unicode": "1F64E-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with pouting face tone3",
"shortname": ":person_with_pouting_face_tone3:",
"category": "people",
@@ -20868,11 +23244,12 @@
"sexy",
"cute",
"annoyed"
- ]
+ ],
+ "moji": "🙎🏽"
},
"person_with_pouting_face_tone4": {
"unicode": "1F64E-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with pouting face tone4",
"shortname": ":person_with_pouting_face_tone4:",
"category": "people",
@@ -20886,11 +23263,12 @@
"sexy",
"cute",
"annoyed"
- ]
+ ],
+ "moji": "🙎🏾"
},
"person_with_pouting_face_tone5": {
"unicode": "1F64E-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with pouting face tone5",
"shortname": ":person_with_pouting_face_tone5:",
"category": "people",
@@ -20904,11 +23282,12 @@
"sexy",
"cute",
"annoyed"
- ]
+ ],
+ "moji": "🙎🏿"
},
"pick": {
"unicode": "26CF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pick",
"shortname": ":pick:",
"category": "objects",
@@ -20917,8 +23296,10 @@
"keywords": [
"mining",
"object",
- "tool"
- ]
+ "tool",
+ "weapon"
+ ],
+ "moji": "⛏"
},
"pig": {
"unicode": "1F437",
@@ -20976,7 +23357,6 @@
"food",
"eat",
"cute",
- "oink",
"pink",
"smell",
"truffle"
@@ -20993,7 +23373,9 @@
"aliases_ascii": [],
"keywords": [
"health",
- "medicine"
+ "medicine",
+ "object",
+ "drugs"
],
"moji": "💊"
},
@@ -21002,7 +23384,7 @@
"unicode_alternates": [],
"name": "pineapple",
"shortname": ":pineapple:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21018,7 +23400,7 @@
},
"ping_pong": {
"unicode": "1F3D3",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "table tennis paddle and ball",
"shortname": ":ping_pong:",
"category": "activity",
@@ -21026,22 +23408,13 @@
":table_tennis:"
],
"aliases_ascii": [],
- "keywords": []
- },
- "piracy": {
- "unicode": "1F572",
- "unicode_alternates": [],
- "name": "no piracy",
- "shortname": ":piracy:",
- "category": "objects_symbols",
- "aliases": [
- ":no_piracy:"
- ],
- "aliases_ascii": [],
"keywords": [
- "theft",
- "rule"
- ]
+ "game",
+ "ball",
+ "sport",
+ "ping pong"
+ ],
+ "moji": "🏓"
},
"pisces": {
"unicode": "2653",
@@ -21050,7 +23423,7 @@
],
"name": "pisces",
"shortname": ":pisces:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21063,9 +23436,8 @@
"zodiac",
"sign",
"purple-square",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♓"
},
@@ -21074,7 +23446,7 @@
"unicode_alternates": [],
"name": "slice of pizza",
"shortname": ":pizza:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21086,13 +23458,14 @@
"italian",
"italy",
"slice",
- "peperoni"
+ "peperoni",
+ "boys night"
],
"moji": "🍕"
},
"place_of_worship": {
"unicode": "1F6D0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "place of worship",
"shortname": ":place_of_worship:",
"category": "symbols",
@@ -21100,11 +23473,16 @@
":worship_symbol:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "religion",
+ "symbol",
+ "pray"
+ ],
+ "moji": "🛐"
},
"play_pause": {
"unicode": "23EF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "black right-pointing double triangle with double vertical bar",
"shortname": ":play_pause:",
"category": "symbols",
@@ -21117,26 +23495,30 @@
"right",
"sound",
"symbol"
- ]
+ ],
+ "moji": "⏯"
},
"point_down": {
"unicode": "1F447",
"unicode_alternates": [],
"name": "white down pointing backhand index",
"shortname": ":point_down:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"direction",
"fingers",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "diversity"
],
"moji": "👇"
},
"point_down_tone1": {
"unicode": "1F447-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white down pointing backhand index tone 1",
"shortname": ":point_down_tone1:",
"category": "people",
@@ -21146,11 +23528,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👇🏻"
},
"point_down_tone2": {
"unicode": "1F447-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white down pointing backhand index tone 2",
"shortname": ":point_down_tone2:",
"category": "people",
@@ -21160,11 +23543,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👇🏼"
},
"point_down_tone3": {
"unicode": "1F447-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white down pointing backhand index tone 3",
"shortname": ":point_down_tone3:",
"category": "people",
@@ -21174,11 +23558,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👇🏽"
},
"point_down_tone4": {
"unicode": "1F447-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white down pointing backhand index tone 4",
"shortname": ":point_down_tone4:",
"category": "people",
@@ -21188,11 +23573,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👇🏾"
},
"point_down_tone5": {
"unicode": "1F447-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white down pointing backhand index tone 5",
"shortname": ":point_down_tone5:",
"category": "people",
@@ -21202,26 +23588,31 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👇🏿"
},
"point_left": {
"unicode": "1F448",
"unicode_alternates": [],
"name": "white left pointing backhand index",
"shortname": ":point_left:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"direction",
"fingers",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "hi",
+ "diversity"
],
"moji": "👈"
},
"point_left_tone1": {
"unicode": "1F448-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white left pointing backhand index tone 1",
"shortname": ":point_left_tone1:",
"category": "people",
@@ -21231,11 +23622,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👈🏻"
},
"point_left_tone2": {
"unicode": "1F448-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white left pointing backhand index tone 2",
"shortname": ":point_left_tone2:",
"category": "people",
@@ -21245,11 +23637,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👈🏼"
},
"point_left_tone3": {
"unicode": "1F448-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white left pointing backhand index tone 3",
"shortname": ":point_left_tone3:",
"category": "people",
@@ -21259,11 +23652,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👈🏽"
},
"point_left_tone4": {
"unicode": "1F448-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white left pointing backhand index tone 4",
"shortname": ":point_left_tone4:",
"category": "people",
@@ -21273,11 +23667,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👈🏾"
},
"point_left_tone5": {
"unicode": "1F448-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white left pointing backhand index tone 5",
"shortname": ":point_left_tone5:",
"category": "people",
@@ -21287,26 +23682,31 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👈🏿"
},
"point_right": {
"unicode": "1F449",
"unicode_alternates": [],
"name": "white right pointing backhand index",
"shortname": ":point_right:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"direction",
"fingers",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "hi",
+ "diversity"
],
"moji": "👉"
},
"point_right_tone1": {
"unicode": "1F449-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white right pointing backhand index tone 1",
"shortname": ":point_right_tone1:",
"category": "people",
@@ -21316,11 +23716,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👉🏻"
},
"point_right_tone2": {
"unicode": "1F449-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white right pointing backhand index tone 2",
"shortname": ":point_right_tone2:",
"category": "people",
@@ -21330,11 +23731,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👉🏼"
},
"point_right_tone3": {
"unicode": "1F449-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white right pointing backhand index tone 3",
"shortname": ":point_right_tone3:",
"category": "people",
@@ -21344,11 +23746,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👉🏽"
},
"point_right_tone4": {
"unicode": "1F449-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white right pointing backhand index tone 4",
"shortname": ":point_right_tone4:",
"category": "people",
@@ -21358,11 +23761,12 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👉🏾"
},
"point_right_tone5": {
"unicode": "1F449-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white right pointing backhand index tone 5",
"shortname": ":point_right_tone5:",
"category": "people",
@@ -21372,7 +23776,8 @@
"direction",
"finger",
"hand"
- ]
+ ],
+ "moji": "👉🏿"
},
"point_up": {
"unicode": "261D",
@@ -21381,13 +23786,17 @@
],
"name": "white up pointing index",
"shortname": ":point_up:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"direction",
"fingers",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "emojione",
+ "diversity"
],
"moji": "☝"
},
@@ -21396,19 +23805,22 @@
"unicode_alternates": [],
"name": "white up pointing backhand index",
"shortname": ":point_up_2:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"direction",
"fingers",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "diversity"
],
"moji": "👆"
},
"point_up_2_tone1": {
"unicode": "1F446-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing backhand index tone 1",
"shortname": ":point_up_2_tone1:",
"category": "people",
@@ -21419,11 +23831,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "👆🏻"
},
"point_up_2_tone2": {
"unicode": "1F446-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing backhand index tone 2",
"shortname": ":point_up_2_tone2:",
"category": "people",
@@ -21434,11 +23847,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "👆🏼"
},
"point_up_2_tone3": {
"unicode": "1F446-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing backhand index tone 3",
"shortname": ":point_up_2_tone3:",
"category": "people",
@@ -21449,11 +23863,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "👆🏽"
},
"point_up_2_tone4": {
"unicode": "1F446-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing backhand index tone 4",
"shortname": ":point_up_2_tone4:",
"category": "people",
@@ -21464,11 +23879,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "👆🏾"
},
"point_up_2_tone5": {
"unicode": "1F446-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing backhand index tone 5",
"shortname": ":point_up_2_tone5:",
"category": "people",
@@ -21479,11 +23895,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "👆🏿"
},
"point_up_tone1": {
"unicode": "261D-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing index tone 1",
"shortname": ":point_up_tone1:",
"category": "people",
@@ -21494,11 +23911,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "☝🏻"
},
"point_up_tone2": {
"unicode": "261D-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing index tone 2",
"shortname": ":point_up_tone2:",
"category": "people",
@@ -21509,11 +23927,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "☝🏼"
},
"point_up_tone3": {
"unicode": "261D-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing index tone 3",
"shortname": ":point_up_tone3:",
"category": "people",
@@ -21524,11 +23943,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "☝🏽"
},
"point_up_tone4": {
"unicode": "261D-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing index tone 4",
"shortname": ":point_up_tone4:",
"category": "people",
@@ -21539,11 +23959,12 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "☝🏾"
},
"point_up_tone5": {
"unicode": "261D-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white up pointing index tone 5",
"shortname": ":point_up_tone5:",
"category": "people",
@@ -21554,14 +23975,15 @@
"finger",
"hand",
"one"
- ]
+ ],
+ "moji": "☝🏿"
},
"police_car": {
"unicode": "1F693",
"unicode_alternates": [],
"name": "police car",
"shortname": ":police_car:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21577,7 +23999,8 @@
"citation",
"crime",
"help",
- "officer"
+ "officer",
+ "911"
],
"moji": "🚓"
},
@@ -21595,7 +24018,6 @@
"dog",
"nature",
"poodle",
- "dog",
"clip",
"showy",
"sophisticated",
@@ -21608,7 +24030,7 @@
"unicode_alternates": [],
"name": "pile of poo",
"shortname": ":poop:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":shit:",
":hankey:",
@@ -21620,32 +24042,41 @@
"shit",
"shitface",
"turd",
- "poo"
+ "poo",
+ "bathroom",
+ "sol",
+ "diarrhea"
],
"moji": "💩"
},
"popcorn": {
"unicode": "1F37F",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "popcorn",
"shortname": ":popcorn:",
- "category": "foods",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "food",
+ "parties"
+ ],
+ "moji": "🍿"
},
"post_office": {
"unicode": "1F3E3",
"unicode_alternates": [],
"name": "japanese post office",
"shortname": ":post_office:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"building",
"communication",
- "email"
+ "email",
+ "places",
+ "post office"
],
"moji": "🏣"
},
@@ -21659,7 +24090,8 @@
"aliases_ascii": [],
"keywords": [
"instrument",
- "music"
+ "music",
+ "object"
],
"moji": "📯"
},
@@ -21674,7 +24106,8 @@
"keywords": [
"email",
"envelope",
- "letter"
+ "letter",
+ "object"
],
"moji": "📮"
},
@@ -21683,7 +24116,7 @@
"unicode_alternates": [],
"name": "potable water symbol",
"shortname": ":potable_water:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21699,27 +24132,40 @@
"clear",
"clean",
"aqua",
- "h20"
+ "h20",
+ "symbol"
],
"moji": "🚰"
},
+ "potato": {
+ "unicode": "1F954",
+ "unicode_alternates": [],
+ "name": "potato",
+ "shortname": ":potato:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥔"
+ },
"pouch": {
"unicode": "1F45D",
"unicode_alternates": [],
"name": "pouch",
"shortname": ":pouch:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"accessories",
"bag",
"pouch",
- "bag",
"cosmetic",
"packing",
"grandma",
- "makeup"
+ "makeup",
+ "women",
+ "fashion"
],
"moji": "👝"
},
@@ -21728,7 +24174,7 @@
"unicode_alternates": [],
"name": "poultry leg",
"shortname": ":poultry_leg:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21737,7 +24183,8 @@
"poultry",
"leg",
"chicken",
- "fried"
+ "fried",
+ "holidays"
],
"moji": "🍗"
},
@@ -21759,13 +24206,9 @@
"uk",
"pound",
"britain",
- "british",
"banknote",
- "money",
- "currency",
"paper",
- "cash",
- "bills"
+ "cash"
],
"moji": "💷"
},
@@ -21774,7 +24217,7 @@
"unicode_alternates": [],
"name": "pouting cat face",
"shortname": ":pouting_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21784,7 +24227,8 @@
"annoyed",
"miffed",
"glower",
- "frown"
+ "frown",
+ "cat"
],
"moji": "😾"
},
@@ -21793,7 +24237,7 @@
"unicode_alternates": [],
"name": "person with folded hands",
"shortname": ":pray:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21807,13 +24251,19 @@
"hands",
"sorrow",
"regret",
- "sorry"
+ "sorry",
+ "body",
+ "hi",
+ "luck",
+ "thank you",
+ "diversity",
+ "scientology"
],
"moji": "🙏"
},
"pray_tone1": {
"unicode": "1F64F-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with folded hands tone 1",
"shortname": ":pray_tone1:",
"category": "people",
@@ -21830,11 +24280,12 @@
"sorrow",
"regret",
"sorry"
- ]
+ ],
+ "moji": "🙏🏻"
},
"pray_tone2": {
"unicode": "1F64F-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with folded hands tone 2",
"shortname": ":pray_tone2:",
"category": "people",
@@ -21851,11 +24302,12 @@
"sorrow",
"regret",
"sorry"
- ]
+ ],
+ "moji": "🙏🏼"
},
"pray_tone3": {
"unicode": "1F64F-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with folded hands tone 3",
"shortname": ":pray_tone3:",
"category": "people",
@@ -21872,11 +24324,12 @@
"sorrow",
"regret",
"sorry"
- ]
+ ],
+ "moji": "🙏🏽"
},
"pray_tone4": {
"unicode": "1F64F-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with folded hands tone 4",
"shortname": ":pray_tone4:",
"category": "people",
@@ -21893,11 +24346,12 @@
"sorrow",
"regret",
"sorry"
- ]
+ ],
+ "moji": "🙏🏾"
},
"pray_tone5": {
"unicode": "1F64F-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person with folded hands tone 5",
"shortname": ":pray_tone5:",
"category": "people",
@@ -21914,24 +24368,173 @@
"sorrow",
"regret",
"sorry"
- ]
+ ],
+ "moji": "🙏🏿"
},
"prayer_beads": {
"unicode": "1F4FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "prayer beads",
"shortname": ":prayer_beads:",
"category": "objects",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "object",
+ "rosary"
+ ],
+ "moji": "📿"
+ },
+ "pregnant_woman": {
+ "unicode": "1F930",
+ "unicode_alternates": [],
+ "name": "pregnant woman",
+ "shortname": ":pregnant_woman:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰"
+ },
+ "pregnant_woman_tone1": {
+ "unicode": "1F930-1F3FB",
+ "unicode_alternates": [],
+ "name": "pregnant woman tone 1",
+ "shortname": ":pregnant_woman_tone1:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰🏻"
+ },
+ "pregnant_woman_tone2": {
+ "unicode": "1F930-1F3FC",
+ "unicode_alternates": [],
+ "name": "pregnant woman tone 2",
+ "shortname": ":pregnant_woman_tone2:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰🏼"
+ },
+ "pregnant_woman_tone3": {
+ "unicode": "1F930-1F3FD",
+ "unicode_alternates": [],
+ "name": "pregnant woman tone 3",
+ "shortname": ":pregnant_woman_tone3:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰🏽"
+ },
+ "pregnant_woman_tone4": {
+ "unicode": "1F930-1F3FE",
+ "unicode_alternates": [],
+ "name": "pregnant woman tone 4",
+ "shortname": ":pregnant_woman_tone4:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰🏾"
+ },
+ "pregnant_woman_tone5": {
+ "unicode": "1F930-1F3FF",
+ "unicode_alternates": [],
+ "name": "pregnant woman tone 5",
+ "shortname": ":pregnant_woman_tone5:",
+ "category": "people",
+ "aliases": [
+ ":expecting_woman_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤰🏿"
+ },
+ "prince": {
+ "unicode": "1F934",
+ "unicode_alternates": [],
+ "name": "prince",
+ "shortname": ":prince:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴"
+ },
+ "prince_tone1": {
+ "unicode": "1F934-1F3FB",
+ "unicode_alternates": [],
+ "name": "prince tone 1",
+ "shortname": ":prince_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴🏻"
+ },
+ "prince_tone2": {
+ "unicode": "1F934-1F3FC",
+ "unicode_alternates": [],
+ "name": "prince tone 2",
+ "shortname": ":prince_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴🏼"
+ },
+ "prince_tone3": {
+ "unicode": "1F934-1F3FD",
+ "unicode_alternates": [],
+ "name": "prince tone 3",
+ "shortname": ":prince_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴🏽"
+ },
+ "prince_tone4": {
+ "unicode": "1F934-1F3FE",
+ "unicode_alternates": [],
+ "name": "prince tone 4",
+ "shortname": ":prince_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴🏾"
+ },
+ "prince_tone5": {
+ "unicode": "1F934-1F3FF",
+ "unicode_alternates": [],
+ "name": "prince tone 5",
+ "shortname": ":prince_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤴🏿"
},
"princess": {
"unicode": "1F478",
"unicode_alternates": [],
"name": "princess",
"shortname": ":princess:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -21947,13 +24550,18 @@
"queen",
"daughter",
"disney",
- "high-maintenance"
+ "high-maintenance",
+ "people",
+ "women",
+ "diversity",
+ "beautiful",
+ "girls night"
],
"moji": "👸"
},
"princess_tone1": {
"unicode": "1F478-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "princess tone 1",
"shortname": ":princess_tone1:",
"category": "people",
@@ -21972,11 +24580,12 @@
"daughter",
"disney",
"high-maintenance"
- ]
+ ],
+ "moji": "👸🏻"
},
"princess_tone2": {
"unicode": "1F478-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "princess tone 2",
"shortname": ":princess_tone2:",
"category": "people",
@@ -21995,11 +24604,12 @@
"daughter",
"disney",
"high-maintenance"
- ]
+ ],
+ "moji": "👸🏼"
},
"princess_tone3": {
"unicode": "1F478-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "princess tone 3",
"shortname": ":princess_tone3:",
"category": "people",
@@ -22018,11 +24628,12 @@
"daughter",
"disney",
"high-maintenance"
- ]
+ ],
+ "moji": "👸🏽"
},
"princess_tone4": {
"unicode": "1F478-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "princess tone 4",
"shortname": ":princess_tone4:",
"category": "people",
@@ -22041,11 +24652,12 @@
"daughter",
"disney",
"high-maintenance"
- ]
+ ],
+ "moji": "👸🏾"
},
"princess_tone5": {
"unicode": "1F478-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "princess tone 5",
"shortname": ":princess_tone5:",
"category": "people",
@@ -22064,49 +24676,34 @@
"daughter",
"disney",
"high-maintenance"
- ]
+ ],
+ "moji": "👸🏿"
},
"printer": {
"unicode": "1F5A8",
"unicode_alternates": [],
"name": "printer",
"shortname": ":printer:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"hardcopy",
"paper",
"inkjet",
- "laser"
- ]
- },
- "prohibited": {
- "unicode": "1F6C7",
- "unicode_alternates": [],
- "name": "prohibited sign",
- "shortname": ":prohibited:",
- "category": "objects_symbols",
- "aliases": [
- ":prohibited_sign:"
+ "laser",
+ "electronics",
+ "work",
+ "office"
],
- "aliases_ascii": [],
- "keywords": [
- "no",
- "not",
- "denied",
- "disallow",
- "forbid",
- "limit",
- "stop"
- ]
+ "moji": "🖨"
},
"projector": {
"unicode": "1F4FD",
"unicode_alternates": [],
"name": "film projector",
"shortname": ":projector:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":film_projector:"
],
@@ -22117,26 +24714,35 @@
"motion",
"picture",
"8mm",
- "16mm"
- ]
+ "16mm",
+ "object",
+ "camera"
+ ],
+ "moji": "📽"
},
"punch": {
"unicode": "1F44A",
"unicode_alternates": [],
"name": "fisted hand sign",
"shortname": ":punch:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fist",
- "hand"
+ "hand",
+ "body",
+ "hands",
+ "hi",
+ "fist bump",
+ "diversity",
+ "boys night"
],
"moji": "👊"
},
"punch_tone1": {
"unicode": "1F44A-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fisted hand sign tone 1",
"shortname": ":punch_tone1:",
"category": "people",
@@ -22145,11 +24751,12 @@
"keywords": [
"fist",
"punch"
- ]
+ ],
+ "moji": "👊🏻"
},
"punch_tone2": {
"unicode": "1F44A-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fisted hand sign tone 2",
"shortname": ":punch_tone2:",
"category": "people",
@@ -22158,11 +24765,12 @@
"keywords": [
"fist",
"punch"
- ]
+ ],
+ "moji": "👊🏼"
},
"punch_tone3": {
"unicode": "1F44A-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fisted hand sign tone 3",
"shortname": ":punch_tone3:",
"category": "people",
@@ -22171,11 +24779,12 @@
"keywords": [
"fist",
"punch"
- ]
+ ],
+ "moji": "👊🏽"
},
"punch_tone4": {
"unicode": "1F44A-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fisted hand sign tone 4",
"shortname": ":punch_tone4:",
"category": "people",
@@ -22184,11 +24793,12 @@
"keywords": [
"fist",
"punch"
- ]
+ ],
+ "moji": "👊🏾"
},
"punch_tone5": {
"unicode": "1F44A-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "fisted hand sign tone 5",
"shortname": ":punch_tone5:",
"category": "people",
@@ -22197,14 +24807,15 @@
"keywords": [
"fist",
"punch"
- ]
+ ],
+ "moji": "👊🏿"
},
"purple_heart": {
"unicode": "1F49C",
"unicode_alternates": [],
"name": "purple heart",
"shortname": ":purple_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22215,7 +24826,6 @@
"purple",
"violet",
"heart",
- "love",
"sensitive",
"understanding",
"compassionate",
@@ -22224,7 +24834,8 @@
"honor",
"royalty",
"veteran",
- "sacrifice"
+ "sacrifice",
+ "symbol"
],
"moji": "💜"
},
@@ -22233,7 +24844,7 @@
"unicode_alternates": [],
"name": "purse",
"shortname": ":purse:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22246,9 +24857,9 @@
"handbag",
"coin bag",
"accessory",
- "money",
"ladies",
- "shopping"
+ "shopping",
+ "women"
],
"moji": "👛"
},
@@ -22261,28 +24872,18 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "stationery"
+ "stationery",
+ "object",
+ "office"
],
"moji": "📌"
},
- "pushpin_black": {
- "unicode": "1F588",
- "unicode_alternates": [],
- "name": "black pushpin",
- "shortname": ":pushpin_black:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "stationery"
- ]
- },
"put_litter_in_its_place": {
"unicode": "1F6AE",
"unicode_alternates": [],
"name": "put litter in its place symbol",
"shortname": ":put_litter_in_its_place:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22292,7 +24893,8 @@
"trash",
"garbage",
"receptacle",
- "can"
+ "can",
+ "symbol"
],
"moji": "🚮"
},
@@ -22301,12 +24903,15 @@
"unicode_alternates": [],
"name": "black question mark ornament",
"shortname": ":question:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"confused",
- "doubt"
+ "doubt",
+ "symbol",
+ "punctuation",
+ "wth"
],
"moji": "❓"
},
@@ -22320,7 +24925,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife"
],
"moji": "🐰"
},
@@ -22339,7 +24945,8 @@
"bunny",
"easter",
"reproduction",
- "prolific"
+ "prolific",
+ "wildlife"
],
"moji": "🐇"
},
@@ -22348,7 +24955,7 @@
"unicode_alternates": [],
"name": "racing car",
"shortname": ":race_car:",
- "category": "activity",
+ "category": "travel",
"aliases": [
":racing_car:"
],
@@ -22359,8 +24966,11 @@
"stock",
"nascar",
"speed",
- "drive"
- ]
+ "drive",
+ "transportation",
+ "car"
+ ],
+ "moji": "🏎"
},
"racehorse": {
"unicode": "1F40E",
@@ -22391,7 +25001,8 @@
"gelding",
"yearling",
"thoroughbred",
- "pony"
+ "pony",
+ "wildlife"
],
"moji": "🐎"
},
@@ -22407,7 +25018,8 @@
"communication",
"music",
"podcast",
- "program"
+ "program",
+ "electronics"
],
"moji": "📻"
},
@@ -22416,17 +25028,19 @@
"unicode_alternates": [],
"name": "radio button",
"shortname": ":radio_button:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "input"
+ "input",
+ "symbol",
+ "circle"
],
"moji": "🔘"
},
"radioactive": {
"unicode": "2622",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "radioactive sign",
"shortname": ":radioactive:",
"category": "symbols",
@@ -22435,15 +25049,17 @@
],
"aliases_ascii": [],
"keywords": [
- "symbol"
- ]
+ "symbol",
+ "science"
+ ],
+ "moji": "☢"
},
"rage": {
"unicode": "1F621",
"unicode_alternates": [],
"name": "pouting face",
"shortname": ":rage:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22454,7 +25070,9 @@
"pout",
"anger",
"rage",
- "irate"
+ "irate",
+ "smiley",
+ "emotion"
],
"moji": "😡"
},
@@ -22463,7 +25081,7 @@
"unicode_alternates": [],
"name": "railway car",
"shortname": ":railway_car:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22473,7 +25091,8 @@
"rail",
"car",
"coach",
- "train"
+ "train",
+ "travel"
],
"moji": "🚃"
},
@@ -22482,7 +25101,7 @@
"unicode_alternates": [],
"name": "railway track",
"shortname": ":railway_track:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [
":railroad_track:"
],
@@ -22492,15 +25111,18 @@
"trolley",
"subway",
"locomotive",
- "transit"
- ]
+ "transit",
+ "travel",
+ "vacation"
+ ],
+ "moji": "🛤"
},
"rainbow": {
"unicode": "1F308",
"unicode_alternates": [],
"name": "rainbow",
"shortname": ":rainbow:",
- "category": "nature",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22516,28 +25138,114 @@
"spectrum",
"refract",
"leprechaun",
- "gold"
+ "gold",
+ "weather",
+ "gay",
+ "rain"
],
"moji": "🌈"
},
+ "raised_back_of_hand": {
+ "unicode": "1F91A",
+ "unicode_alternates": [],
+ "name": "raised back of hand",
+ "shortname": ":raised_back_of_hand:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚"
+ },
+ "raised_back_of_hand_tone1": {
+ "unicode": "1F91A-1F3FB",
+ "unicode_alternates": [],
+ "name": "raised back of hand tone 1",
+ "shortname": ":raised_back_of_hand_tone1:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚🏻"
+ },
+ "raised_back_of_hand_tone2": {
+ "unicode": "1F91A-1F3FC",
+ "unicode_alternates": [],
+ "name": "raised back of hand tone 2",
+ "shortname": ":raised_back_of_hand_tone2:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand_tone2:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚🏼"
+ },
+ "raised_back_of_hand_tone3": {
+ "unicode": "1F91A-1F3FD",
+ "unicode_alternates": [],
+ "name": "raised back of hand tone 3",
+ "shortname": ":raised_back_of_hand_tone3:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚🏽"
+ },
+ "raised_back_of_hand_tone4": {
+ "unicode": "1F91A-1F3FE",
+ "unicode_alternates": [],
+ "name": "raised back of hand tone 4",
+ "shortname": ":raised_back_of_hand_tone4:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚🏾"
+ },
+ "raised_back_of_hand_tone5": {
+ "unicode": "1F91A-1F3FF",
+ "unicode_alternates": [],
+ "name": "raised back of hand tone 5",
+ "shortname": ":raised_back_of_hand_tone5:",
+ "category": "people",
+ "aliases": [
+ ":back_of_hand_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤚🏿"
+ },
"raised_hand": {
"unicode": "270B",
"unicode_alternates": [],
"name": "raised hand",
"shortname": ":raised_hand:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"female",
"girl",
- "woman"
+ "woman",
+ "body",
+ "hands",
+ "hi",
+ "diversity",
+ "girls night"
],
"moji": "✋"
},
"raised_hand_tone1": {
"unicode": "270B-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand tone 1",
"shortname": ":raised_hand_tone1:",
"category": "people",
@@ -22547,11 +25255,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "✋🏻"
},
"raised_hand_tone2": {
"unicode": "270B-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand tone 2",
"shortname": ":raised_hand_tone2:",
"category": "people",
@@ -22561,11 +25270,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "✋🏼"
},
"raised_hand_tone3": {
"unicode": "270B-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand tone 3",
"shortname": ":raised_hand_tone3:",
"category": "people",
@@ -22575,11 +25285,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "✋🏽"
},
"raised_hand_tone4": {
"unicode": "270B-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand tone 4",
"shortname": ":raised_hand_tone4:",
"category": "people",
@@ -22589,11 +25300,12 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "✋🏾"
},
"raised_hand_tone5": {
"unicode": "270B-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand tone 5",
"shortname": ":raised_hand_tone5:",
"category": "people",
@@ -22603,14 +25315,15 @@
"female",
"girl",
"woman"
- ]
+ ],
+ "moji": "✋🏿"
},
"raised_hands": {
"unicode": "1F64C",
"unicode_alternates": [],
"name": "person raising both hands in celebration",
"shortname": ":raised_hands:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22619,13 +25332,19 @@
"winning",
"woot",
"yay",
- "banzai"
+ "banzai",
+ "body",
+ "hands",
+ "diversity",
+ "perfect",
+ "good",
+ "parties"
],
"moji": "🙌"
},
"raised_hands_tone1": {
"unicode": "1F64C-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person raising both hands in celebration tone 1",
"shortname": ":raised_hands_tone1:",
"category": "people",
@@ -22639,11 +25358,12 @@
"yay",
"banzai",
"raised"
- ]
+ ],
+ "moji": "🙌🏻"
},
"raised_hands_tone2": {
"unicode": "1F64C-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person raising both hands in celebration tone 2",
"shortname": ":raised_hands_tone2:",
"category": "people",
@@ -22657,11 +25377,12 @@
"yay",
"banzai",
"raised"
- ]
+ ],
+ "moji": "🙌🏼"
},
"raised_hands_tone3": {
"unicode": "1F64C-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person raising both hands in celebration tone 3",
"shortname": ":raised_hands_tone3:",
"category": "people",
@@ -22675,11 +25396,12 @@
"yay",
"banzai",
"raised"
- ]
+ ],
+ "moji": "🙌🏽"
},
"raised_hands_tone4": {
"unicode": "1F64C-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person raising both hands in celebration tone 4",
"shortname": ":raised_hands_tone4:",
"category": "people",
@@ -22693,11 +25415,12 @@
"yay",
"banzai",
"raised"
- ]
+ ],
+ "moji": "🙌🏾"
},
"raised_hands_tone5": {
"unicode": "1F64C-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "person raising both hands in celebration tone 5",
"shortname": ":raised_hands_tone5:",
"category": "people",
@@ -22711,14 +25434,15 @@
"yay",
"banzai",
"raised"
- ]
+ ],
+ "moji": "🙌🏿"
},
"raising_hand": {
"unicode": "1F64B",
"unicode_alternates": [],
"name": "happy person raising one hand",
"shortname": ":raising_hand:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22729,13 +25453,16 @@
"raise",
"notice",
"attention",
- "answer"
+ "answer",
+ "people",
+ "women",
+ "diversity"
],
"moji": "🙋"
},
"raising_hand_tone1": {
"unicode": "1F64B-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "happy person raising one hand tone1",
"shortname": ":raising_hand_tone1:",
"category": "people",
@@ -22749,11 +25476,12 @@
"notice",
"attention",
"answer"
- ]
+ ],
+ "moji": "🙋🏻"
},
"raising_hand_tone2": {
"unicode": "1F64B-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "happy person raising one hand tone2",
"shortname": ":raising_hand_tone2:",
"category": "people",
@@ -22767,11 +25495,12 @@
"notice",
"attention",
"answer"
- ]
+ ],
+ "moji": "🙋🏼"
},
"raising_hand_tone3": {
"unicode": "1F64B-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "happy person raising one hand tone3",
"shortname": ":raising_hand_tone3:",
"category": "people",
@@ -22785,11 +25514,12 @@
"notice",
"attention",
"answer"
- ]
+ ],
+ "moji": "🙋🏽"
},
"raising_hand_tone4": {
"unicode": "1F64B-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "happy person raising one hand tone4",
"shortname": ":raising_hand_tone4:",
"category": "people",
@@ -22803,11 +25533,12 @@
"notice",
"attention",
"answer"
- ]
+ ],
+ "moji": "🙋🏾"
},
"raising_hand_tone5": {
"unicode": "1F64B-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "happy person raising one hand tone5",
"shortname": ":raising_hand_tone5:",
"category": "people",
@@ -22821,7 +25552,8 @@
"notice",
"attention",
"answer"
- ]
+ ],
+ "moji": "🙋🏿"
},
"ram": {
"unicode": "1F40F",
@@ -22836,10 +25568,10 @@
"nature",
"sheep",
"ram",
- "sheep",
"male",
"horn",
- "horns"
+ "horns",
+ "wildlife"
],
"moji": "🐏"
},
@@ -22848,7 +25580,7 @@
"unicode_alternates": [],
"name": "steaming bowl",
"shortname": ":ramen:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22860,7 +25592,8 @@
"noodles",
"bowl",
"steaming",
- "soup"
+ "soup",
+ "japan"
],
"moji": "🍜"
},
@@ -22884,7 +25617,7 @@
},
"record_button": {
"unicode": "23FA",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "black circle for record",
"shortname": ":record_button:",
"category": "symbols",
@@ -22892,8 +25625,10 @@
"aliases_ascii": [],
"keywords": [
"sound",
- "symbol"
- ]
+ "symbol",
+ "circle"
+ ],
+ "moji": "⏺"
},
"recycle": {
"unicode": "267B",
@@ -22902,14 +25637,15 @@
],
"name": "black universal recycling symbol",
"shortname": ":recycle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
"environment",
"garbage",
- "trash"
+ "trash",
+ "symbol"
],
"moji": "♻"
},
@@ -22918,12 +25654,14 @@
"unicode_alternates": [],
"name": "automobile",
"shortname": ":red_car:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"transportation",
- "vehicle"
+ "vehicle",
+ "car",
+ "travel"
],
"moji": "🚗"
},
@@ -22932,11 +25670,14 @@
"unicode_alternates": [],
"name": "large red circle",
"shortname": ":red_circle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "circle"
],
"moji": "🔴"
},
@@ -22946,12 +25687,13 @@
"unicode_alternates": [],
"name": "registered sign",
"shortname": ":registered:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"alphabet",
- "circle"
+ "circle",
+ "symbol"
]
},
"relaxed": {
@@ -22961,7 +25703,7 @@
],
"name": "white smiling face",
"shortname": ":relaxed:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22969,7 +25711,9 @@
"face",
"happiness",
"massage",
- "smile"
+ "smile",
+ "happy",
+ "smiley"
],
"moji": "☺"
},
@@ -22978,7 +25722,7 @@
"unicode_alternates": [],
"name": "relieved face",
"shortname": ":relieved:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -22989,8 +25733,9 @@
"relaxed",
"relieved",
"satisfied",
- "phew",
- "relief"
+ "relief",
+ "smiley",
+ "emotion"
],
"moji": "😌"
},
@@ -22999,24 +25744,28 @@
"unicode_alternates": [],
"name": "reminder ribbon",
"shortname": ":reminder_ribbon:",
- "category": "celebration",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "awareness"
- ]
+ "awareness",
+ "award"
+ ],
+ "moji": "🎗"
},
"repeat": {
"unicode": "1F501",
"unicode_alternates": [],
"name": "clockwise rightwards and leftwards open circle arr",
"shortname": ":repeat:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"loop",
- "record"
+ "record",
+ "arrow",
+ "symbol"
],
"moji": "🔁"
},
@@ -23025,12 +25774,14 @@
"unicode_alternates": [],
"name": "clockwise rightwards and leftwards open circle arr",
"shortname": ":repeat_one:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "loop"
+ "loop",
+ "arrow",
+ "symbol"
],
"moji": "🔂"
},
@@ -23039,7 +25790,7 @@
"unicode_alternates": [],
"name": "restroom",
"shortname": ":restroom:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23051,7 +25802,8 @@
"restroom",
"sign",
"shared",
- "toilet"
+ "toilet",
+ "symbol"
],
"moji": "🚻"
},
@@ -23060,7 +25812,7 @@
"unicode_alternates": [],
"name": "revolving hearts",
"shortname": ":revolving_hearts:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23074,7 +25826,8 @@
"moving",
"circle",
"multiple",
- "lovers"
+ "lovers",
+ "symbol"
],
"moji": "💞"
},
@@ -23083,21 +25836,36 @@
"unicode_alternates": [],
"name": "black left-pointing double triangle",
"shortname": ":rewind:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "play"
+ "play",
+ "arrow",
+ "symbol"
],
"moji": "⏪"
},
+ "rhino": {
+ "unicode": "1F98F",
+ "unicode_alternates": [],
+ "name": "rhinoceros",
+ "shortname": ":rhino:",
+ "category": "nature",
+ "aliases": [
+ ":rhinoceros:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦏"
+ },
"ribbon": {
"unicode": "1F380",
"unicode_alternates": [],
"name": "ribbon",
"shortname": ":ribbon:",
- "category": "emoticons",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23108,7 +25876,10 @@
"ribbon",
"lace",
"wrap",
- "decorate"
+ "decorate",
+ "object",
+ "gift",
+ "birthday"
],
"moji": "🎀"
},
@@ -23117,7 +25888,7 @@
"unicode_alternates": [],
"name": "cooked rice",
"shortname": ":rice:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23125,8 +25896,9 @@
"rice",
"white",
"grain",
- "food",
- "bowl"
+ "bowl",
+ "sushi",
+ "japan"
],
"moji": "🍚"
},
@@ -23135,7 +25907,7 @@
"unicode_alternates": [],
"name": "rice ball",
"shortname": ":rice_ball:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23146,7 +25918,8 @@
"white",
"nori",
"seaweed",
- "japanese"
+ "sushi",
+ "japan"
],
"moji": "🍙"
},
@@ -23155,7 +25928,7 @@
"unicode_alternates": [],
"name": "rice cracker",
"shortname": ":rice_cracker:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23164,8 +25937,7 @@
"rice",
"cracker",
"seaweed",
- "food",
- "japanese"
+ "sushi"
],
"moji": "🍘"
},
@@ -23174,7 +25946,7 @@
"unicode_alternates": [],
"name": "moon viewing ceremony",
"shortname": ":rice_scene:",
- "category": "objects",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23187,92 +25959,115 @@
"rice",
"scene",
"festival",
- "autumn"
+ "autumn",
+ "places",
+ "space",
+ "sky",
+ "travel"
],
"moji": "🎑"
},
- "right_speaker": {
- "unicode": "1F568",
+ "right_facing_fist": {
+ "unicode": "1F91C",
"unicode_alternates": [],
- "name": "right speaker",
- "shortname": ":right_speaker:",
- "category": "objects_symbols",
- "aliases": [],
+ "name": "right-facing fist",
+ "shortname": ":right_facing_fist:",
+ "category": "people",
+ "aliases": [
+ ":right_fist:"
+ ],
"aliases_ascii": [],
- "keywords": [
- "sound",
- "listen",
- "hear",
- "noise",
- "volume"
- ]
+ "keywords": [],
+ "moji": "🤜"
},
- "right_speaker_one": {
- "unicode": "1F569",
+ "right_facing_fist_tone1": {
+ "unicode": "1F91C-1F3FB",
"unicode_alternates": [],
- "name": "right speaker with one sound wave",
- "shortname": ":right_speaker_one:",
- "category": "objects_symbols",
+ "name": "right facing fist tone 1",
+ "shortname": ":right_facing_fist_tone1:",
+ "category": "people",
"aliases": [
- ":right_speaker_with_one_sound_wave:"
+ ":right_fist_tone1:"
],
"aliases_ascii": [],
- "keywords": [
- "low",
- "volume"
- ]
+ "keywords": [],
+ "moji": "🤜🏻"
},
- "right_speaker_three": {
- "unicode": "1F56A",
+ "right_facing_fist_tone2": {
+ "unicode": "1F91C-1F3FC",
"unicode_alternates": [],
- "name": "right speaker with three sound waves",
- "shortname": ":right_speaker_three:",
- "category": "objects_symbols",
+ "name": "right facing fist tone 2",
+ "shortname": ":right_facing_fist_tone2:",
+ "category": "people",
"aliases": [
- ":right_speaker_with_three_sound_waves:"
+ ":right_fist_tone2:"
],
"aliases_ascii": [],
- "keywords": [
- "loud",
- "high",
- "volume"
- ]
+ "keywords": [],
+ "moji": "🤜🏼"
+ },
+ "right_facing_fist_tone3": {
+ "unicode": "1F91C-1F3FD",
+ "unicode_alternates": [],
+ "name": "right facing fist tone 3",
+ "shortname": ":right_facing_fist_tone3:",
+ "category": "people",
+ "aliases": [
+ ":right_fist_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤜🏽"
+ },
+ "right_facing_fist_tone4": {
+ "unicode": "1F91C-1F3FE",
+ "unicode_alternates": [],
+ "name": "right facing fist tone 4",
+ "shortname": ":right_facing_fist_tone4:",
+ "category": "people",
+ "aliases": [
+ ":right_fist_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤜🏾"
+ },
+ "right_facing_fist_tone5": {
+ "unicode": "1F91C-1F3FF",
+ "unicode_alternates": [],
+ "name": "right facing fist tone 5",
+ "shortname": ":right_facing_fist_tone5:",
+ "category": "people",
+ "aliases": [
+ ":right_fist_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤜🏿"
},
"ring": {
"unicode": "1F48D",
"unicode_alternates": [],
"name": "ring",
"shortname": ":ring:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"marriage",
"propose",
"valentines",
- "wedding"
+ "wedding",
+ "object",
+ "fashion",
+ "gem",
+ "accessories"
],
"moji": "💍"
},
- "ringing_bell": {
- "unicode": "1F56D",
- "unicode_alternates": [],
- "name": "ringing bell",
- "shortname": ":ringing_bell:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "alert",
- "ding",
- "volume",
- "sound",
- "chime"
- ]
- },
"robot": {
"unicode": "1F916",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "robot face",
"shortname": ":robot:",
"category": "people",
@@ -23280,14 +26075,18 @@
":robot_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "monster",
+ "robot"
+ ],
+ "moji": "🤖"
},
"rocket": {
"unicode": "1F680",
"unicode_alternates": [],
"name": "rocket",
"shortname": ":rocket:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23298,16 +26097,33 @@
"space",
"spacecraft",
"astronaut",
- "cosmonaut"
+ "cosmonaut",
+ "transportation",
+ "object",
+ "fly",
+ "blast"
],
"moji": "🚀"
},
+ "rofl": {
+ "unicode": "1F923",
+ "unicode_alternates": [],
+ "name": "rolling on the floor laughing",
+ "shortname": ":rofl:",
+ "category": "people",
+ "aliases": [
+ ":rolling_on_the_floor_laughing:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤣"
+ },
"roller_coaster": {
"unicode": "1F3A2",
"unicode_alternates": [],
"name": "roller coaster",
"shortname": ":roller_coaster:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23322,13 +26138,16 @@
"park",
"fair",
"ride",
- "entertainment"
+ "entertainment",
+ "places",
+ "vacation",
+ "roller coaster"
],
"moji": "🎢"
},
"rolling_eyes": {
"unicode": "1F644",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with rolling eyes",
"shortname": ":rolling_eyes:",
"category": "people",
@@ -23336,7 +26155,14 @@
":face_with_rolling_eyes:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "mad",
+ "smiley",
+ "rolling eyes",
+ "emotion",
+ "sarcastic"
+ ],
+ "moji": "🙄"
},
"rooster": {
"unicode": "1F413",
@@ -23375,9 +26201,13 @@
"fragrant",
"flower",
"thorns",
- "love",
"petals",
- "romance"
+ "romance",
+ "nature",
+ "plant",
+ "rip",
+ "condolence",
+ "beautiful"
],
"moji": "🌹"
},
@@ -23386,31 +26216,21 @@
"unicode_alternates": [],
"name": "rosette",
"shortname": ":rosette:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "flower"
- ]
- },
- "rosette_black": {
- "unicode": "1F3F6",
- "unicode_alternates": [],
- "name": "black rosette",
- "shortname": ":rosette_black:",
- "category": "objects_symbols",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "flower"
- ]
+ "flower",
+ "tropical"
+ ],
+ "moji": "🏵"
},
"rotating_light": {
"unicode": "1F6A8",
"unicode_alternates": [],
"name": "police cars revolving light",
"shortname": ":rotating_light:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23419,8 +26239,8 @@
"emergency",
"police",
"light",
- "police",
- "emergency"
+ "transportation",
+ "object"
],
"moji": "🚨"
},
@@ -23429,11 +26249,13 @@
"unicode_alternates": [],
"name": "round pushpin",
"shortname": ":round_pushpin:",
- "category": "places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "stationery"
+ "stationery",
+ "object",
+ "office"
],
"moji": "📍"
},
@@ -23442,7 +26264,7 @@
"unicode_alternates": [],
"name": "rowboat",
"shortname": ":rowboat:",
- "category": "places",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23453,13 +26275,18 @@
"boat",
"row",
"oar",
- "paddle"
+ "paddle",
+ "men",
+ "workout",
+ "sport",
+ "rowing",
+ "diversity"
],
"moji": "🚣"
},
"rowboat_tone1": {
"unicode": "1F6A3-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "rowboat tone 1",
"shortname": ":rowboat_tone1:",
"category": "activity",
@@ -23473,11 +26300,12 @@
"row",
"oar",
"paddle"
- ]
+ ],
+ "moji": "🚣🏻"
},
"rowboat_tone2": {
"unicode": "1F6A3-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "rowboat tone 2",
"shortname": ":rowboat_tone2:",
"category": "activity",
@@ -23491,11 +26319,12 @@
"row",
"oar",
"paddle"
- ]
+ ],
+ "moji": "🚣🏼"
},
"rowboat_tone3": {
"unicode": "1F6A3-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "rowboat tone 3",
"shortname": ":rowboat_tone3:",
"category": "activity",
@@ -23509,11 +26338,12 @@
"row",
"oar",
"paddle"
- ]
+ ],
+ "moji": "🚣🏽"
},
"rowboat_tone4": {
"unicode": "1F6A3-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "rowboat tone 4",
"shortname": ":rowboat_tone4:",
"category": "activity",
@@ -23527,11 +26357,12 @@
"row",
"oar",
"paddle"
- ]
+ ],
+ "moji": "🚣🏾"
},
"rowboat_tone5": {
"unicode": "1F6A3-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "rowboat tone 5",
"shortname": ":rowboat_tone5:",
"category": "activity",
@@ -23545,14 +26376,15 @@
"row",
"oar",
"paddle"
- ]
+ ],
+ "moji": "🚣🏿"
},
"rugby_football": {
"unicode": "1F3C9",
"unicode_alternates": [],
"name": "rugby football",
"shortname": ":rugby_football:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23562,7 +26394,8 @@
"ball",
"sport",
"team",
- "england"
+ "england",
+ "game"
],
"moji": "🏉"
},
@@ -23571,7 +26404,7 @@
"unicode_alternates": [],
"name": "runner",
"shortname": ":runner:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23581,16 +26414,19 @@
"run",
"runner",
"jog",
- "exercise",
"sprint",
"race",
- "dash"
+ "dash",
+ "people",
+ "men",
+ "diversity",
+ "boys night"
],
"moji": "🏃"
},
"runner_tone1": {
"unicode": "1F3C3-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "runner tone 1",
"shortname": ":runner_tone1:",
"category": "people",
@@ -23605,11 +26441,12 @@
"race",
"dash",
"marathon"
- ]
+ ],
+ "moji": "🏃🏻"
},
"runner_tone2": {
"unicode": "1F3C3-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "runner tone 2",
"shortname": ":runner_tone2:",
"category": "people",
@@ -23624,11 +26461,12 @@
"race",
"dash",
"marathon"
- ]
+ ],
+ "moji": "🏃🏼"
},
"runner_tone3": {
"unicode": "1F3C3-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "runner tone 3",
"shortname": ":runner_tone3:",
"category": "people",
@@ -23643,11 +26481,12 @@
"race",
"dash",
"marathon"
- ]
+ ],
+ "moji": "🏃🏽"
},
"runner_tone4": {
"unicode": "1F3C3-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "runner tone 4",
"shortname": ":runner_tone4:",
"category": "people",
@@ -23662,11 +26501,12 @@
"race",
"dash",
"marathon"
- ]
+ ],
+ "moji": "🏃🏾"
},
"runner_tone5": {
"unicode": "1F3C3-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "runner tone 5",
"shortname": ":runner_tone5:",
"category": "people",
@@ -23681,14 +26521,15 @@
"race",
"dash",
"marathon"
- ]
+ ],
+ "moji": "🏃🏿"
},
"running_shirt_with_sash": {
"unicode": "1F3BD",
"unicode_alternates": [],
"name": "running shirt with sash",
"shortname": ":running_shirt_with_sash:",
- "category": "emoticons",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23699,13 +26540,14 @@
"shirt",
"cloths",
"compete",
- "sports"
+ "sports",
+ "award"
],
"moji": "🎽"
},
"sa": {
"unicode": "1F202",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "squared katakana sa",
"shortname": ":sa:",
"category": "symbols",
@@ -23716,7 +26558,8 @@
"japanese",
"symbol",
"word"
- ]
+ ],
+ "moji": "🈂"
},
"sagittarius": {
"unicode": "2650",
@@ -23725,7 +26568,7 @@
],
"name": "sagittarius",
"shortname": ":sagittarius:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23738,9 +26581,8 @@
"stars",
"zodiac",
"sign",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♐"
},
@@ -23751,12 +26593,15 @@
],
"name": "sailboat",
"shortname": ":sailboat:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"ship",
- "transportation"
+ "transportation",
+ "travel",
+ "boat",
+ "vacation"
],
"moji": "⛵"
},
@@ -23765,7 +26610,7 @@
"unicode_alternates": [],
"name": "sake bottle and cup",
"shortname": ":sake:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23774,26 +26619,41 @@
"drunk",
"wine",
"sake",
- "wine",
"rice",
"ferment",
"alcohol",
"japanese",
- "drink"
+ "japan",
+ "girls night"
],
"moji": "🍶"
},
+ "salad": {
+ "unicode": "1F957",
+ "unicode_alternates": [],
+ "name": "green salad",
+ "shortname": ":salad:",
+ "category": "food",
+ "aliases": [
+ ":green_salad:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥗"
+ },
"sandal": {
"unicode": "1F461",
"unicode_alternates": [],
"name": "womans sandal",
"shortname": ":sandal:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"fashion",
- "shoes"
+ "shoes",
+ "shoe",
+ "accessories"
],
"moji": "👡"
},
@@ -23802,7 +26662,7 @@
"unicode_alternates": [],
"name": "father christmas",
"shortname": ":santa:",
- "category": "objects",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23823,14 +26683,18 @@
"nice",
"sleigh",
"father",
- "christmas",
- "holiday"
+ "holiday",
+ "people",
+ "hat",
+ "winter",
+ "holidays",
+ "diversity"
],
"moji": "🎅"
},
"santa_tone1": {
"unicode": "1F385-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "father christmas tone 1",
"shortname": ":santa_tone1:",
"category": "people",
@@ -23852,11 +26716,12 @@
"nice",
"sleigh",
"holiday"
- ]
+ ],
+ "moji": "🎅🏻"
},
"santa_tone2": {
"unicode": "1F385-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "father christmas tone 2",
"shortname": ":santa_tone2:",
"category": "people",
@@ -23878,11 +26743,12 @@
"nice",
"sleigh",
"holiday"
- ]
+ ],
+ "moji": "🎅🏼"
},
"santa_tone3": {
"unicode": "1F385-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "father christmas tone 3",
"shortname": ":santa_tone3:",
"category": "people",
@@ -23904,11 +26770,12 @@
"nice",
"sleigh",
"holiday"
- ]
+ ],
+ "moji": "🎅🏽"
},
"santa_tone4": {
"unicode": "1F385-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "father christmas tone 4",
"shortname": ":santa_tone4:",
"category": "people",
@@ -23930,11 +26797,12 @@
"nice",
"sleigh",
"holiday"
- ]
+ ],
+ "moji": "🎅🏾"
},
"santa_tone5": {
"unicode": "1F385-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "father christmas tone 5",
"shortname": ":santa_tone5:",
"category": "people",
@@ -23956,7 +26824,8 @@
"nice",
"sleigh",
"holiday"
- ]
+ ],
+ "moji": "🎅🏿"
},
"satellite": {
"unicode": "1F4E1",
@@ -23967,7 +26836,8 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "communication"
+ "communication",
+ "object"
],
"moji": "📡"
},
@@ -23976,21 +26846,23 @@
"unicode_alternates": [],
"name": "satellite",
"shortname": ":satellite_orbital:",
- "category": "objects_symbols",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"communication",
"orbital",
- "space"
- ]
+ "space",
+ "object"
+ ],
+ "moji": "🛰"
},
"saxophone": {
"unicode": "1F3B7",
"unicode_alternates": [],
"name": "saxophone",
"shortname": ":saxophone:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -23998,15 +26870,14 @@
"music",
"saxophone",
"sax",
- "music",
- "instrument",
- "woodwind"
+ "woodwind",
+ "instruments"
],
"moji": "🎷"
},
"scales": {
"unicode": "2696",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "scales",
"shortname": ":scales:",
"category": "objects",
@@ -24020,14 +26891,15 @@
"tool",
"weight",
"zodiac"
- ]
+ ],
+ "moji": "⚖"
},
"school": {
"unicode": "1F3EB",
"unicode_alternates": [],
"name": "school",
"shortname": ":school:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24039,7 +26911,8 @@
"high",
"college",
"teach",
- "education"
+ "education",
+ "places"
],
"moji": "🏫"
},
@@ -24048,7 +26921,7 @@
"unicode_alternates": [],
"name": "school satchel",
"shortname": ":school_satchel:",
- "category": "objects",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24058,14 +26931,16 @@
"school",
"satchel",
"backpack",
- "bag",
"packing",
"pack",
"hike",
- "education",
"adventure",
"travel",
- "sightsee"
+ "sightsee",
+ "fashion",
+ "office",
+ "vacation",
+ "accessories"
],
"moji": "🎒"
},
@@ -24081,19 +26956,39 @@
"aliases_ascii": [],
"keywords": [
"cut",
- "stationery"
+ "stationery",
+ "object",
+ "tool",
+ "weapon",
+ "office"
],
"moji": "✂"
},
+ "scooter": {
+ "unicode": "1F6F4",
+ "unicode_alternates": [],
+ "name": "scooter",
+ "shortname": ":scooter:",
+ "category": "travel",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🛴"
+ },
"scorpion": {
"unicode": "1F982",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "scorpion",
"shortname": ":scorpion:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "insects",
+ "reptile",
+ "animal"
+ ],
+ "moji": "🦂"
},
"scorpius": {
"unicode": "264F",
@@ -24102,7 +26997,7 @@
],
"name": "scorpius",
"shortname": ":scorpius:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24115,9 +27010,8 @@
"stars",
"zodiac",
"sign",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♏"
},
@@ -24126,7 +27020,7 @@
"unicode_alternates": [],
"name": "face screaming in fear",
"shortname": ":scream:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24135,7 +27029,12 @@
"scream",
"painting",
"artist",
- "alien"
+ "alien",
+ "smiley",
+ "surprised",
+ "wow",
+ "emotion",
+ "omg"
],
"moji": "😱"
},
@@ -24144,7 +27043,7 @@
"unicode_alternates": [],
"name": "weary cat face",
"shortname": ":scream_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24161,7 +27060,8 @@
"exhausted",
"scream",
"painting",
- "artist"
+ "artist",
+ "cat"
],
"moji": "🙀"
},
@@ -24174,7 +27074,9 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "documents"
+ "documents",
+ "object",
+ "office"
],
"moji": "📜"
},
@@ -24183,14 +27085,31 @@
"unicode_alternates": [],
"name": "seat",
"shortname": ":seat:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "sit"
+ "sit",
+ "transportation",
+ "object",
+ "travel",
+ "vacation"
],
"moji": "💺"
},
+ "second_place": {
+ "unicode": "1F948",
+ "unicode_alternates": [],
+ "name": "second place medal",
+ "shortname": ":second_place:",
+ "category": "activity",
+ "aliases": [
+ ":second_place_medal:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥈"
+ },
"secret": {
"unicode": "3299",
"unicode_alternates": [
@@ -24198,11 +27117,13 @@
],
"name": "circled ideograph secret",
"shortname": ":secret:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "privacy"
+ "privacy",
+ "japan",
+ "symbol"
],
"moji": "㊙"
},
@@ -24211,14 +27132,13 @@
"unicode_alternates": [],
"name": "see-no-evil monkey",
"shortname": ":see_no_evil:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"animal",
"monkey",
"nature",
- "monkey",
"see",
"eyes",
"vision",
@@ -24241,34 +27161,118 @@
"nature",
"plant",
"seedling",
- "plant",
"new",
"start",
- "grow"
+ "grow",
+ "leaf"
],
"moji": "🌱"
},
+ "selfie": {
+ "unicode": "1F933",
+ "unicode_alternates": [],
+ "name": "selfie",
+ "shortname": ":selfie:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳"
+ },
+ "selfie_tone1": {
+ "unicode": "1F933-1F3FB",
+ "unicode_alternates": [],
+ "name": "selfie tone 1",
+ "shortname": ":selfie_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳🏻"
+ },
+ "selfie_tone2": {
+ "unicode": "1F933-1F3FC",
+ "unicode_alternates": [],
+ "name": "selfie tone 2",
+ "shortname": ":selfie_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳🏼"
+ },
+ "selfie_tone3": {
+ "unicode": "1F933-1F3FD",
+ "unicode_alternates": [],
+ "name": "selfie tone 3",
+ "shortname": ":selfie_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳🏽"
+ },
+ "selfie_tone4": {
+ "unicode": "1F933-1F3FE",
+ "unicode_alternates": [],
+ "name": "selfie tone 4",
+ "shortname": ":selfie_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳🏾"
+ },
+ "selfie_tone5": {
+ "unicode": "1F933-1F3FF",
+ "unicode_alternates": [],
+ "name": "selfie tone 5",
+ "shortname": ":selfie_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤳🏿"
+ },
"seven": {
"moji": "7️⃣",
"unicode": "0037-20E3",
"unicode_alternates": [
"0037-FE0F-20E3"
],
- "name": "digit seven",
+ "name": "keycap digit seven",
"shortname": ":seven:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"7",
"blue-square",
"numbers",
- "prime"
+ "prime",
+ "number",
+ "math",
+ "symbol"
]
},
+ "shallow_pan_of_food": {
+ "unicode": "1F958",
+ "unicode_alternates": [],
+ "name": "shallow pan of food",
+ "shortname": ":shallow_pan_of_food:",
+ "category": "food",
+ "aliases": [
+ ":paella:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "pan of food"
+ ],
+ "moji": "🥘"
+ },
"shamrock": {
"unicode": "2618",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "shamrock",
"shortname": ":shamrock:",
"category": "nature",
@@ -24276,15 +27280,29 @@
"aliases_ascii": [],
"keywords": [
"nature",
- "plant"
- ]
+ "plant",
+ "luck",
+ "leaf"
+ ],
+ "moji": "☘"
+ },
+ "shark": {
+ "unicode": "1F988",
+ "unicode_alternates": [],
+ "name": "shark",
+ "shortname": ":shark:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦈"
},
"shaved_ice": {
"unicode": "1F367",
"unicode_alternates": [],
"name": "shaved ice",
"shortname": ":shaved_ice:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24295,7 +27313,8 @@
"dessert",
"treat",
"syrup",
- "flavoring"
+ "flavoring",
+ "food"
],
"moji": "🍧"
},
@@ -24334,7 +27353,6 @@
"sea",
"shell",
"spiral",
- "beach",
"sand",
"crab",
"nautilus"
@@ -24346,7 +27364,7 @@
"unicode_alternates": [],
"name": "shield",
"shortname": ":shield:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24354,12 +27372,13 @@
"route",
"sign",
"highway",
- "interstate"
- ]
+ "object"
+ ],
+ "moji": "🛡"
},
"shinto_shrine": {
"unicode": "26E9",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "shinto shrine",
"shortname": ":shinto_shrine:",
"category": "travel",
@@ -24367,15 +27386,20 @@
"aliases_ascii": [],
"keywords": [
"religion",
- "symbol"
- ]
+ "symbol",
+ "places",
+ "building",
+ "travel",
+ "vacation"
+ ],
+ "moji": "⛩"
},
"ship": {
"unicode": "1F6A2",
"unicode_alternates": [],
"name": "ship",
"shortname": ":ship:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24383,7 +27407,9 @@
"transportation",
"ferry",
"ship",
- "boat"
+ "boat",
+ "travel",
+ "vacation"
],
"moji": "🚢"
},
@@ -24392,7 +27418,7 @@
"unicode_alternates": [],
"name": "t-shirt",
"shortname": ":shirt:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24406,7 +27432,7 @@
"unicode_alternates": [],
"name": "shopping bags",
"shortname": ":shopping_bags:",
- "category": "travel_places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24414,8 +27440,25 @@
"mall",
"buy",
"store",
- "shop"
- ]
+ "shop",
+ "object",
+ "birthday",
+ "parties"
+ ],
+ "moji": "🛍"
+ },
+ "shopping_cart": {
+ "unicode": "1F6D2",
+ "unicode_alternates": [],
+ "name": "shopping trolley",
+ "shortname": ":shopping_cart:",
+ "category": "objects",
+ "aliases": [
+ ":shopping_trolley:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🛒"
},
"shower": {
"unicode": "1F6BF",
@@ -24433,22 +27476,100 @@
"shower",
"soap",
"water",
- "clean",
"shampoo",
- "lather"
+ "lather",
+ "object"
],
"moji": "🚿"
},
+ "shrimp": {
+ "unicode": "1F990",
+ "unicode_alternates": [],
+ "name": "shrimp",
+ "shortname": ":shrimp:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦐"
+ },
+ "shrug": {
+ "unicode": "1F937",
+ "unicode_alternates": [],
+ "name": "shrug",
+ "shortname": ":shrug:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷"
+ },
+ "shrug_tone1": {
+ "unicode": "1F937-1F3FB",
+ "unicode_alternates": [],
+ "name": "shrug tone 1",
+ "shortname": ":shrug_tone1:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷🏻"
+ },
+ "shrug_tone2": {
+ "unicode": "1F937-1F3FC",
+ "unicode_alternates": [],
+ "name": "shrug tone 2",
+ "shortname": ":shrug_tone2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷🏼"
+ },
+ "shrug_tone3": {
+ "unicode": "1F937-1F3FD",
+ "unicode_alternates": [],
+ "name": "shrug tone 3",
+ "shortname": ":shrug_tone3:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷🏽"
+ },
+ "shrug_tone4": {
+ "unicode": "1F937-1F3FE",
+ "unicode_alternates": [],
+ "name": "shrug tone 4",
+ "shortname": ":shrug_tone4:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷🏾"
+ },
+ "shrug_tone5": {
+ "unicode": "1F937-1F3FF",
+ "unicode_alternates": [],
+ "name": "shrug tone 5",
+ "shortname": ":shrug_tone5:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤷🏿"
+ },
"signal_strength": {
"unicode": "1F4F6",
"unicode_alternates": [],
"name": "antenna with bars",
"shortname": ":signal_strength:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "📶"
},
@@ -24458,15 +27579,18 @@
"unicode_alternates": [
"0036-FE0F-20E3"
],
- "name": "digit six",
+ "name": "keycap digit six",
"shortname": ":six:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"6",
"blue-square",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"six_pointed_star": {
@@ -24474,11 +27598,15 @@
"unicode_alternates": [],
"name": "six pointed star with middle dot",
"shortname": ":six_pointed_star:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "purple-square"
+ "purple-square",
+ "religion",
+ "jew",
+ "star",
+ "symbol"
],
"moji": "🔯"
},
@@ -24487,7 +27615,7 @@
"unicode_alternates": [],
"name": "ski and ski boot",
"shortname": ":ski:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24499,18 +27627,19 @@
"cross-country",
"poles",
"snow",
- "winter",
"mountain",
"alpine",
"powder",
"slalom",
- "freestyle"
+ "freestyle",
+ "sport",
+ "skiing"
],
"moji": "🎿"
},
"skier": {
"unicode": "26F7",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "skier",
"shortname": ":skier:",
"category": "activity",
@@ -24521,15 +27650,20 @@
"ski",
"snow",
"sport",
- "travel"
- ]
+ "travel",
+ "hat",
+ "vacation",
+ "cold",
+ "skiing"
+ ],
+ "moji": "⛷"
},
"skull": {
"unicode": "1F480",
"unicode_alternates": [],
"name": "skull",
"shortname": ":skull:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":skeleton:"
],
@@ -24537,13 +27671,15 @@
"keywords": [
"dead",
"skeleton",
- "dying"
+ "dying",
+ "halloween",
+ "skull"
],
"moji": "💀"
},
"skull_crossbones": {
"unicode": "2620",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "skull and crossbones",
"shortname": ":skull_crossbones:",
"category": "objects",
@@ -24556,15 +27692,19 @@
"death",
"face",
"monster",
- "person"
- ]
+ "person",
+ "symbol",
+ "dead",
+ "skull"
+ ],
+ "moji": "☠"
},
"sleeping": {
"unicode": "1F634",
"unicode_alternates": [],
"name": "sleeping face",
"shortname": ":sleeping:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24572,9 +27712,11 @@
"sleepy",
"tired",
"sleep",
- "sleepy",
"sleeping",
- "snore"
+ "snore",
+ "smiley",
+ "emotion",
+ "goodnight"
],
"moji": "😴"
},
@@ -24583,21 +27725,23 @@
"unicode_alternates": [],
"name": "sleeping accommodation",
"shortname": ":sleeping_accommodation:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"hotel",
"motel",
- "rest"
- ]
+ "rest",
+ "tired"
+ ],
+ "moji": "🛌"
},
"sleepy": {
"unicode": "1F62A",
"unicode_alternates": [],
"name": "sleepy face",
"shortname": ":sleepy:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24605,8 +27749,10 @@
"rest",
"tired",
"sleepy",
- "tired",
- "exhausted"
+ "exhausted",
+ "smiley",
+ "sick",
+ "emotion"
],
"moji": "😪"
},
@@ -24624,8 +27770,12 @@
"slight",
"frown",
"unhappy",
- "disappointed"
- ]
+ "disappointed",
+ "sad",
+ "smiley",
+ "emotion"
+ ],
+ "moji": "🙁"
},
"slight_smile": {
"unicode": "1F642",
@@ -24640,15 +27790,17 @@
"keywords": [
"slight",
"smile",
- "happy"
- ]
+ "happy",
+ "smiley"
+ ],
+ "moji": "🙂"
},
"slot_machine": {
"unicode": "1F3B0",
"unicode_alternates": [],
"name": "slot machine",
"shortname": ":slot_machine:",
- "category": "places",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24657,10 +27809,11 @@
"vegas",
"slot",
"machine",
- "gamble",
"one-armed bandit",
"slots",
- "luck"
+ "luck",
+ "game",
+ "boys night"
],
"moji": "🎰"
},
@@ -24669,11 +27822,13 @@
"unicode_alternates": [],
"name": "small blue diamond",
"shortname": ":small_blue_diamond:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol"
],
"moji": "🔹"
},
@@ -24682,11 +27837,13 @@
"unicode_alternates": [],
"name": "small orange diamond",
"shortname": ":small_orange_diamond:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol"
],
"moji": "🔸"
},
@@ -24695,11 +27852,14 @@
"unicode_alternates": [],
"name": "up-pointing red triangle",
"shortname": ":small_red_triangle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "triangle"
],
"moji": "🔺"
},
@@ -24708,11 +27868,14 @@
"unicode_alternates": [],
"name": "down-pointing red triangle",
"shortname": ":small_red_triangle_down:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "triangle"
],
"moji": "🔻"
},
@@ -24721,7 +27884,7 @@
"unicode_alternates": [],
"name": "smiling face with open mouth and smiling eyes",
"shortname": ":smile:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":)",
@@ -24739,7 +27902,8 @@
"laugh",
"smile",
"smiley",
- "smiling"
+ "smiling",
+ "emotion"
],
"moji": "😄"
},
@@ -24748,7 +27912,7 @@
"unicode_alternates": [],
"name": "grinning cat face with smiling eyes",
"shortname": ":smile_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24757,7 +27921,8 @@
"cat",
"smile",
"grin",
- "grinning"
+ "grinning",
+ "happy"
],
"moji": "😸"
},
@@ -24766,7 +27931,7 @@
"unicode_alternates": [],
"name": "smiling face with open mouth",
"shortname": ":smiley:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":D",
@@ -24780,7 +27945,9 @@
"joy",
"smiling",
"smile",
- "smiley"
+ "smiley",
+ "emotion",
+ "good"
],
"moji": "😃"
},
@@ -24789,7 +27956,7 @@
"unicode_alternates": [],
"name": "smiling cat face with open mouth",
"shortname": ":smiley_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24798,8 +27965,7 @@
"happy",
"smile",
"smiley",
- "cat",
- "happy"
+ "cat"
],
"moji": "😺"
},
@@ -24808,16 +27974,19 @@
"unicode_alternates": [],
"name": "smiling face with horns",
"shortname": ":smiling_imp:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"devil",
"horns",
- "horns",
- "devil",
"impish",
- "trouble"
+ "trouble",
+ "silly",
+ "smiley",
+ "angry",
+ "monster",
+ "boys night"
],
"moji": "😈"
},
@@ -24826,7 +27995,7 @@
"unicode_alternates": [],
"name": "smirking face",
"shortname": ":smirk:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24836,10 +28005,12 @@
"smug",
"smirking",
"smirk",
- "smug",
- "smile",
"half-smile",
- "conceited"
+ "conceited",
+ "silly",
+ "smiley",
+ "sexy",
+ "sarcastic"
],
"moji": "😏"
},
@@ -24848,7 +28019,7 @@
"unicode_alternates": [],
"name": "cat face with wry smile",
"shortname": ":smirk_cat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24858,7 +28029,8 @@
"smirking",
"wry",
"confident",
- "confidence"
+ "confidence",
+ "cat"
],
"moji": "😼"
},
@@ -24875,13 +28047,14 @@
"kills",
"tobacco",
"smoking",
- "cigarette",
"smoke",
"cancer",
"lungs",
"inhale",
"tar",
- "nicotine"
+ "nicotine",
+ "symbol",
+ "drugs"
],
"moji": "🚬"
},
@@ -24898,10 +28071,10 @@
"shell",
"slow",
"snail",
- "slow",
"escargot",
"french",
- "appetizer"
+ "appetizer",
+ "insects"
],
"moji": "🐌"
},
@@ -24915,16 +28088,32 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "evil"
+ "evil",
+ "wildlife",
+ "reptile",
+ "creationism"
],
"moji": "🐍"
},
+ "sneezing_face": {
+ "unicode": "1F927",
+ "unicode_alternates": [],
+ "name": "sneezing face",
+ "shortname": ":sneezing_face:",
+ "category": "people",
+ "aliases": [
+ ":sneeze:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤧"
+ },
"snowboarder": {
"unicode": "1F3C2",
"unicode_alternates": [],
"name": "snowboarder",
"shortname": ":snowboarder:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -24932,13 +28121,16 @@
"winter",
"snow",
"boarding",
- "sports",
"freestyle",
"halfpipe",
"board",
"mountain",
"alpine",
- "winter"
+ "hat",
+ "vacation",
+ "cold",
+ "sport",
+ "snowboarding"
],
"moji": "🏂"
},
@@ -24965,13 +28157,13 @@
"droplet",
"ice",
"crystal",
- "cold",
"chilly",
- "winter",
"unique",
"special",
"below zero",
- "elsa"
+ "elsa",
+ "sky",
+ "holidays"
],
"moji": "❄"
},
@@ -24991,13 +28183,15 @@
"season",
"weather",
"winter",
- "xmas"
+ "xmas",
+ "holidays",
+ "snow"
],
"moji": "⛄"
},
"snowman2": {
"unicode": "2603",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "snowman",
"shortname": ":snowman2:",
"category": "nature",
@@ -25007,15 +28201,19 @@
"cold",
"nature",
"snow",
- "weather"
- ]
+ "weather",
+ "winter",
+ "holidays",
+ "christmas"
+ ],
+ "moji": "☃"
},
"sob": {
"unicode": "1F62D",
"unicode_alternates": [],
"name": "loudly crying face",
"shortname": ":sob:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25024,14 +28222,14 @@
"sad",
"tears",
"upset",
- "cry",
"sob",
- "tears",
- "sad",
"melancholy",
"morn",
"somber",
- "hurt"
+ "hurt",
+ "smiley",
+ "emotion",
+ "heartbreak"
],
"moji": "😭"
},
@@ -25042,7 +28240,7 @@
],
"name": "soccer ball",
"shortname": ":soccer:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25051,7 +28249,10 @@
"football",
"sports",
"european",
- "football"
+ "game",
+ "ball",
+ "sport",
+ "soccer"
],
"moji": "⚽"
},
@@ -25060,12 +28261,13 @@
"unicode_alternates": [],
"name": "soon with rightwards arrow above",
"shortname": ":soon:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arrow",
- "words"
+ "words",
+ "symbol"
],
"moji": "🔜"
},
@@ -25074,14 +28276,15 @@
"unicode_alternates": [],
"name": "squared sos",
"shortname": ":sos:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"emergency",
"help",
"red-square",
- "words"
+ "words",
+ "symbol"
],
"moji": "🆘"
},
@@ -25090,12 +28293,14 @@
"unicode_alternates": [],
"name": "speaker with one sound wave",
"shortname": ":sound:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"speaker",
- "volume"
+ "volume",
+ "alarm",
+ "symbol"
],
"moji": "🔉"
},
@@ -25104,12 +28309,14 @@
"unicode_alternates": [],
"name": "alien monster",
"shortname": ":space_invader:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"arcade",
- "game"
+ "game",
+ "monster",
+ "alien"
],
"moji": "👾"
},
@@ -25120,12 +28327,14 @@
],
"name": "black spade suit",
"shortname": ":spades:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cards",
- "poker"
+ "poker",
+ "symbol",
+ "game"
],
"moji": "♠"
},
@@ -25134,7 +28343,7 @@
"unicode_alternates": [],
"name": "spaghetti",
"shortname": ":spaghetti:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25145,7 +28354,7 @@
"noodles",
"tomato",
"sauce",
- "italian"
+ "pasta"
],
"moji": "🍝"
},
@@ -25156,12 +28365,13 @@
],
"name": "sparkle",
"shortname": ":sparkle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"green-square",
- "stars"
+ "stars",
+ "symbol"
],
"moji": "❇"
},
@@ -25170,13 +28380,14 @@
"unicode_alternates": [],
"name": "firework sparkler",
"shortname": ":sparkler:",
- "category": "objects",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"night",
"shine",
- "stars"
+ "stars",
+ "parties"
],
"moji": "🎇"
},
@@ -25185,14 +28396,16 @@
"unicode_alternates": [],
"name": "sparkles",
"shortname": ":sparkles:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"cool",
"shine",
"shiny",
- "stars"
+ "stars",
+ "star",
+ "girls night"
],
"moji": "✨"
},
@@ -25201,14 +28414,16 @@
"unicode_alternates": [],
"name": "sparkling heart",
"shortname": ":sparkling_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"affection",
"like",
"love",
- "valentines"
+ "valentines",
+ "symbol",
+ "girls night"
],
"moji": "💖"
},
@@ -25217,13 +28432,12 @@
"unicode_alternates": [],
"name": "speak-no-evil monkey",
"shortname": ":speak_no_evil:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"animal",
"monkey",
- "monkey",
"mouth",
"talk",
"say",
@@ -25240,36 +28454,41 @@
"unicode_alternates": [],
"name": "speaker",
"shortname": ":speaker:",
- "category": "objects",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"sound",
"listen",
"hear",
- "noise"
- ]
+ "noise",
+ "alarm",
+ "symbol"
+ ],
+ "moji": "🔈"
},
"speaking_head": {
"unicode": "1F5E3",
"unicode_alternates": [],
"name": "speaking head in silhouette",
"shortname": ":speaking_head:",
- "category": "objects_symbols",
+ "category": "people",
"aliases": [
":speaking_head_in_silhouette:"
],
"aliases_ascii": [],
"keywords": [
- "talk"
- ]
+ "talk",
+ "people"
+ ],
+ "moji": "🗣"
},
"speech_balloon": {
"unicode": "1F4AC",
"unicode_alternates": [],
"name": "speech balloon",
"shortname": ":speech_balloon:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25281,96 +28500,18 @@
"conversation",
"communication",
"comic",
- "dialogue"
+ "dialogue",
+ "symbol",
+ "free speech"
],
"moji": "💬"
},
- "speech_left": {
- "unicode": "1F5E8",
- "unicode_alternates": [],
- "name": "left speech bubble",
- "shortname": ":speech_left:",
- "category": "objects_symbols",
- "aliases": [
- ":left_speech_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "words",
- "talk",
- "conversation",
- "communication",
- "comic",
- "dialogue"
- ]
- },
- "speech_right": {
- "unicode": "1F5E9",
- "unicode_alternates": [],
- "name": "right speech bubble",
- "shortname": ":speech_right:",
- "category": "objects_symbols",
- "aliases": [
- ":right_speech_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "words",
- "talk",
- "conversation",
- "communication",
- "comic",
- "dialogue"
- ]
- },
- "speech_three": {
- "unicode": "1F5EB",
- "unicode_alternates": [],
- "name": "three speech bubbles",
- "shortname": ":speech_three:",
- "category": "objects_symbols",
- "aliases": [
- ":three_speech_bubbles:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "words",
- "talk",
- "conversation",
- "communication",
- "comic",
- "dialogue"
- ]
- },
- "speech_two": {
- "unicode": "1F5EA",
- "unicode_alternates": [],
- "name": "two speech bubbles",
- "shortname": ":speech_two:",
- "category": "objects_symbols",
- "aliases": [
- ":two_speech_bubbles:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "words",
- "talk",
- "conversation",
- "communication",
- "comic",
- "dialogue"
- ]
- },
"speedboat": {
"unicode": "1F6A4",
"unicode_alternates": [],
"name": "speedboat",
"shortname": ":speedboat:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25381,7 +28522,10 @@
"speed",
"ski",
"power",
- "boat"
+ "boat",
+ "travel",
+ "vacation",
+ "tropical"
],
"moji": "🚤"
},
@@ -25395,8 +28539,12 @@
"aliases_ascii": [],
"keywords": [
"arachnid",
- "eight-legged"
- ]
+ "eight-legged",
+ "insects",
+ "halloween",
+ "animal"
+ ],
+ "moji": "🕷"
},
"spider_web": {
"unicode": "1F578",
@@ -25407,8 +28555,21 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "cobweb"
- ]
+ "cobweb",
+ "halloween"
+ ],
+ "moji": "🕸"
+ },
+ "spoon": {
+ "unicode": "1F944",
+ "unicode_alternates": [],
+ "name": "spoon",
+ "shortname": ":spoon:",
+ "category": "food",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥄"
},
"spy": {
"unicode": "1F575",
@@ -25423,12 +28584,19 @@
"keywords": [
"pi",
"undercover",
- "investigator"
- ]
+ "investigator",
+ "people",
+ "hat",
+ "men",
+ "glasses",
+ "diversity",
+ "job"
+ ],
+ "moji": "🕵"
},
"spy_tone1": {
"unicode": "1F575-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sleuth or spy tone 1",
"shortname": ":spy_tone1:",
"category": "people",
@@ -25441,11 +28609,12 @@
"undercover",
"investigator",
"person"
- ]
+ ],
+ "moji": "🕵🏻"
},
"spy_tone2": {
"unicode": "1F575-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sleuth or spy tone 2",
"shortname": ":spy_tone2:",
"category": "people",
@@ -25458,11 +28627,12 @@
"undercover",
"investigator",
"person"
- ]
+ ],
+ "moji": "🕵🏼"
},
"spy_tone3": {
"unicode": "1F575-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sleuth or spy tone 3",
"shortname": ":spy_tone3:",
"category": "people",
@@ -25475,11 +28645,12 @@
"undercover",
"investigator",
"person"
- ]
+ ],
+ "moji": "🕵🏽"
},
"spy_tone4": {
"unicode": "1F575-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sleuth or spy tone 4",
"shortname": ":spy_tone4:",
"category": "people",
@@ -25492,11 +28663,12 @@
"undercover",
"investigator",
"person"
- ]
+ ],
+ "moji": "🕵🏾"
},
"spy_tone5": {
"unicode": "1F575-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "sleuth or spy tone 5",
"shortname": ":spy_tone5:",
"category": "people",
@@ -25509,14 +28681,26 @@
"undercover",
"investigator",
"person"
- ]
+ ],
+ "moji": "🕵🏿"
+ },
+ "squid": {
+ "unicode": "1F991",
+ "unicode_alternates": [],
+ "name": "squid",
+ "shortname": ":squid:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🦑"
},
"stadium": {
"unicode": "1F3DF",
"unicode_alternates": [],
"name": "stadium",
"shortname": ":stadium:",
- "category": "travel_places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25524,8 +28708,14 @@
"event",
"concert",
"convention",
- "game"
- ]
+ "game",
+ "places",
+ "building",
+ "travel",
+ "vacation",
+ "boys night"
+ ],
+ "moji": "🏟"
},
"star": {
"unicode": "2B50",
@@ -25539,7 +28729,10 @@
"aliases_ascii": [],
"keywords": [
"night",
- "yellow"
+ "yellow",
+ "space",
+ "sky",
+ "star"
],
"moji": "⭐"
},
@@ -25548,7 +28741,7 @@
"unicode_alternates": [],
"name": "glowing star",
"shortname": ":star2:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25559,13 +28752,15 @@
"star",
"five",
"points",
- "classic"
+ "classic",
+ "space",
+ "sky"
],
"moji": "🌟"
},
"star_and_crescent": {
"unicode": "262A",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "star and crescent",
"shortname": ":star_and_crescent:",
"category": "symbols",
@@ -25576,11 +28771,12 @@
"muslim",
"religion",
"symbol"
- ]
+ ],
+ "moji": "☪"
},
"star_of_david": {
"unicode": "2721",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "star of david",
"shortname": ":star_of_david:",
"category": "symbols",
@@ -25590,15 +28786,17 @@
"jew",
"jewish",
"religion",
- "symbol"
- ]
+ "symbol",
+ "star"
+ ],
+ "moji": "✡"
},
"stars": {
"unicode": "1F320",
"unicode_alternates": [],
"name": "shooting star",
"shortname": ":stars:",
- "category": "nature",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25608,9 +28806,9 @@
"shoot",
"star",
"sky",
- "night",
"comet",
- "meteoroid"
+ "meteoroid",
+ "space"
],
"moji": "🌠"
},
@@ -25619,7 +28817,7 @@
"unicode_alternates": [],
"name": "station",
"shortname": ":station:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25628,7 +28826,8 @@
"vehicle",
"station",
"train",
- "subway"
+ "subway",
+ "travel"
],
"moji": "🚉"
},
@@ -25637,12 +28836,18 @@
"unicode_alternates": [],
"name": "statue of liberty",
"shortname": ":statue_of_liberty:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"american",
- "newyork"
+ "newyork",
+ "places",
+ "america",
+ "travel",
+ "vacation",
+ "statue of liberty",
+ "free speech"
],
"moji": "🗽"
},
@@ -25651,7 +28856,7 @@
"unicode_alternates": [],
"name": "steam locomotive",
"shortname": ":steam_locomotive:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25660,35 +28865,17 @@
"vehicle",
"locomotive",
"steam",
- "train",
- "engine"
+ "engine",
+ "travel"
],
"moji": "🚂"
},
- "stereo": {
- "unicode": "1F4FE",
- "unicode_alternates": [],
- "name": "portable stereo",
- "shortname": ":stereo:",
- "category": "objects_symbols",
- "aliases": [
- ":portable_stereo:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "music",
- "program",
- "boom",
- "box"
- ]
- },
"stew": {
"unicode": "1F372",
"unicode_alternates": [],
"name": "pot of food",
"shortname": ":stew:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25699,28 +28886,14 @@
"soup",
"thick",
"hot",
- "pot"
+ "pot",
+ "steam"
],
"moji": "🍲"
},
- "stock_chart": {
- "unicode": "1F5E0",
- "unicode_alternates": [],
- "name": "stock chart",
- "shortname": ":stock_chart:",
- "category": "objects_symbols",
- "aliases": [],
- "aliases_ascii": [],
- "keywords": [
- "graph",
- "presentation",
- "stats",
- "business"
- ]
- },
"stop_button": {
"unicode": "23F9",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "black square for stop",
"shortname": ":stop_button:",
"category": "symbols",
@@ -25728,12 +28901,14 @@
"aliases_ascii": [],
"keywords": [
"sound",
- "symbol"
- ]
+ "symbol",
+ "square"
+ ],
+ "moji": "⏹"
},
"stopwatch": {
"unicode": "23F1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "stopwatch",
"shortname": ":stopwatch:",
"category": "objects",
@@ -25742,8 +28917,10 @@
"keywords": [
"clock",
"object",
- "time"
- ]
+ "time",
+ "electronics"
+ ],
+ "moji": "⏱"
},
"straight_ruler": {
"unicode": "1F4CF",
@@ -25754,7 +28931,10 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "stationery"
+ "stationery",
+ "object",
+ "tool",
+ "office"
],
"moji": "📏"
},
@@ -25763,7 +28943,7 @@
"unicode_alternates": [],
"name": "strawberry",
"shortname": ":strawberry:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25782,7 +28962,7 @@
"unicode_alternates": [],
"name": "face with stuck-out tongue",
"shortname": ":stuck_out_tongue:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
":P",
@@ -25807,8 +28987,10 @@
"prank",
"tongue",
"silly",
- "playful",
- "cheeky"
+ "cheeky",
+ "smiley",
+ "sex",
+ "emotion"
],
"moji": "😛"
},
@@ -25817,7 +28999,7 @@
"unicode_alternates": [],
"name": "face with stuck-out tongue and tightly-closed eyes",
"shortname": ":stuck_out_tongue_closed_eyes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25828,8 +29010,10 @@
"tongue",
"kidding",
"silly",
- "playful",
- "ecstatic"
+ "ecstatic",
+ "happy",
+ "smiley",
+ "emotion"
],
"moji": "😝"
},
@@ -25838,7 +29022,7 @@
"unicode_alternates": [],
"name": "face with stuck-out tongue and winking eye",
"shortname": ":stuck_out_tongue_winking_eye:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
">:P",
@@ -25856,11 +29040,27 @@
"winking",
"kidding",
"silly",
- "playful",
- "crazy"
+ "crazy",
+ "happy",
+ "smiley",
+ "emotion",
+ "parties"
],
"moji": "😜"
},
+ "stuffed_flatbread": {
+ "unicode": "1F959",
+ "unicode_alternates": [],
+ "name": "stuffed flatbread",
+ "shortname": ":stuffed_flatbread:",
+ "category": "food",
+ "aliases": [
+ ":stuffed_pita:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥙"
+ },
"sun_with_face": {
"unicode": "1F31E",
"unicode_alternates": [],
@@ -25874,7 +29074,9 @@
"sun",
"anthropomorphic",
"face",
- "sky"
+ "sky",
+ "day",
+ "hump day"
],
"moji": "🌞"
},
@@ -25902,7 +29104,7 @@
"unicode_alternates": [],
"name": "smiling face with sunglasses",
"shortname": ":sunglasses:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"B-)",
@@ -25920,8 +29122,11 @@
"sun",
"glasses",
"sunny",
- "cool",
- "smooth"
+ "smooth",
+ "silly",
+ "smiley",
+ "emojione",
+ "boys night"
],
"moji": "😎"
},
@@ -25937,15 +29142,21 @@
"aliases_ascii": [],
"keywords": [
"brightness",
- "weather"
- ]
+ "weather",
+ "sky",
+ "day",
+ "sun",
+ "hot",
+ "morning"
+ ],
+ "moji": "☀"
},
"sunrise": {
"unicode": "1F305",
"unicode_alternates": [],
"name": "sunrise",
"shortname": ":sunrise:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25955,9 +29166,13 @@
"view",
"sunrise",
"sun",
- "morning",
"color",
- "sky"
+ "sky",
+ "places",
+ "travel",
+ "tropical",
+ "day",
+ "hump day"
],
"moji": "🌅"
},
@@ -25966,7 +29181,7 @@
"unicode_alternates": [],
"name": "sunrise over mountains",
"shortname": ":sunrise_over_mountains:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25979,7 +29194,11 @@
"mountain",
"rural",
"color",
- "sky"
+ "sky",
+ "places",
+ "travel",
+ "day",
+ "camp"
],
"moji": "🌄"
},
@@ -25988,7 +29207,7 @@
"unicode_alternates": [],
"name": "surfer",
"shortname": ":surfer:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -25998,15 +29217,19 @@
"surfer",
"surf",
"wave",
- "ocean",
"ride",
- "swell"
+ "swell",
+ "men",
+ "vacation",
+ "tropical",
+ "sport",
+ "diversity"
],
"moji": "🏄"
},
"surfer_tone1": {
"unicode": "1F3C4-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "surfer tone 1",
"shortname": ":surfer_tone1:",
"category": "activity",
@@ -26018,14 +29241,14 @@
"sport",
"surf",
"wave",
- "ocean",
"ride",
"swell"
- ]
+ ],
+ "moji": "🏄🏻"
},
"surfer_tone2": {
"unicode": "1F3C4-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "surfer tone 2",
"shortname": ":surfer_tone2:",
"category": "activity",
@@ -26037,14 +29260,14 @@
"sport",
"surf",
"wave",
- "ocean",
"ride",
"swell"
- ]
+ ],
+ "moji": "🏄🏼"
},
"surfer_tone3": {
"unicode": "1F3C4-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "surfer tone 3",
"shortname": ":surfer_tone3:",
"category": "activity",
@@ -26056,14 +29279,14 @@
"sport",
"surf",
"wave",
- "ocean",
"ride",
"swell"
- ]
+ ],
+ "moji": "🏄🏽"
},
"surfer_tone4": {
"unicode": "1F3C4-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "surfer tone 4",
"shortname": ":surfer_tone4:",
"category": "activity",
@@ -26075,14 +29298,14 @@
"sport",
"surf",
"wave",
- "ocean",
"ride",
"swell"
- ]
+ ],
+ "moji": "🏄🏾"
},
"surfer_tone5": {
"unicode": "1F3C4-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "surfer tone 5",
"shortname": ":surfer_tone5:",
"category": "activity",
@@ -26094,17 +29317,17 @@
"sport",
"surf",
"wave",
- "ocean",
"ride",
"swell"
- ]
+ ],
+ "moji": "🏄🏿"
},
"sushi": {
"unicode": "1F363",
"unicode_alternates": [],
"name": "sushi",
"shortname": ":sushi:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26114,7 +29337,7 @@
"fish",
"raw",
"nigiri",
- "japanese"
+ "japan"
],
"moji": "🍣"
},
@@ -26123,7 +29346,7 @@
"unicode_alternates": [],
"name": "suspension railway",
"shortname": ":suspension_railway:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26133,7 +29356,7 @@
"railway",
"rail",
"train",
- "transportation"
+ "travel"
],
"moji": "🚟"
},
@@ -26142,7 +29365,7 @@
"unicode_alternates": [],
"name": "face with cold sweat",
"shortname": ":sweat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"':(",
@@ -26158,7 +29381,11 @@
"clammy",
"diaphoresis",
"face",
- "hot"
+ "hot",
+ "sad",
+ "smiley",
+ "stressed",
+ "emotion"
],
"moji": "😓"
},
@@ -26167,11 +29394,14 @@
"unicode_alternates": [],
"name": "splashing sweat symbol",
"shortname": ":sweat_drops:",
- "category": "emoticons",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "water"
+ "water",
+ "rain",
+ "stressed",
+ "sweat"
],
"moji": "💦"
},
@@ -26180,7 +29410,7 @@
"unicode_alternates": [],
"name": "smiling face with open mouth and cold sweat",
"shortname": ":sweat_smile:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
"':)",
@@ -26197,7 +29427,10 @@
"smiling",
"cold",
"sweat",
- "perspiration"
+ "perspiration",
+ "smiley",
+ "workout",
+ "emotion"
],
"moji": "😅"
},
@@ -26206,7 +29439,7 @@
"unicode_alternates": [],
"name": "roasted sweet potato",
"shortname": ":sweet_potato:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26216,7 +29449,8 @@
"potato",
"potassium",
"roasted",
- "roast"
+ "roast",
+ "vegetables"
],
"moji": "🍠"
},
@@ -26225,7 +29459,7 @@
"unicode_alternates": [],
"name": "swimmer",
"shortname": ":swimmer:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26238,13 +29472,16 @@
"freestyle",
"butterfly",
"breaststroke",
- "backstroke"
+ "backstroke",
+ "workout",
+ "sport",
+ "diversity"
],
"moji": "🏊"
},
"swimmer_tone1": {
"unicode": "1F3CA-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "swimmer tone 1",
"shortname": ":swimmer_tone1:",
"category": "activity",
@@ -26260,11 +29497,12 @@
"butterfly",
"breaststroke",
"backstroke"
- ]
+ ],
+ "moji": "🏊🏻"
},
"swimmer_tone2": {
"unicode": "1F3CA-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "swimmer tone 2",
"shortname": ":swimmer_tone2:",
"category": "activity",
@@ -26280,11 +29518,12 @@
"butterfly",
"breaststroke",
"backstroke"
- ]
+ ],
+ "moji": "🏊🏼"
},
"swimmer_tone3": {
"unicode": "1F3CA-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "swimmer tone 3",
"shortname": ":swimmer_tone3:",
"category": "activity",
@@ -26300,11 +29539,12 @@
"butterfly",
"breaststroke",
"backstroke"
- ]
+ ],
+ "moji": "🏊🏽"
},
"swimmer_tone4": {
"unicode": "1F3CA-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "swimmer tone 4",
"shortname": ":swimmer_tone4:",
"category": "activity",
@@ -26320,11 +29560,12 @@
"butterfly",
"breaststroke",
"backstroke"
- ]
+ ],
+ "moji": "🏊🏾"
},
"swimmer_tone5": {
"unicode": "1F3CA-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "swimmer tone 5",
"shortname": ":swimmer_tone5:",
"category": "activity",
@@ -26340,30 +29581,40 @@
"butterfly",
"breaststroke",
"backstroke"
- ]
+ ],
+ "moji": "🏊🏿"
},
"symbols": {
"unicode": "1F523",
"unicode_alternates": [],
"name": "input symbol for symbols",
"shortname": ":symbols:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "🔣"
},
"synagogue": {
"unicode": "1F54D",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "synagogue",
"shortname": ":synagogue:",
"category": "travel",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "places",
+ "religion",
+ "building",
+ "travel",
+ "vacation",
+ "condolence"
+ ],
+ "moji": "🕍"
},
"syringe": {
"unicode": "1F489",
@@ -26379,19 +29630,26 @@
"health",
"hospital",
"medicine",
- "needle"
+ "needle",
+ "object",
+ "weapon"
],
"moji": "💉"
},
"taco": {
"unicode": "1F32E",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "taco",
"shortname": ":taco:",
- "category": "foods",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "food",
+ "mexican",
+ "vagina"
+ ],
+ "moji": "🌮"
},
"tada": {
"unicode": "1F389",
@@ -26404,14 +29662,21 @@
"keywords": [
"contulations",
"party",
- "party",
"popper",
"tada",
"celebration",
"victory",
"announcement",
"climax",
- "congratulations"
+ "congratulations",
+ "object",
+ "birthday",
+ "holidays",
+ "cheers",
+ "good",
+ "girls night",
+ "boys night",
+ "parties"
],
"moji": "🎉"
},
@@ -26420,7 +29685,7 @@
"unicode_alternates": [],
"name": "tanabata tree",
"shortname": ":tanabata_tree:",
- "category": "objects",
+ "category": "nature",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26431,7 +29696,8 @@
"festival",
"star",
"wish",
- "holiday"
+ "holiday",
+ "trees"
],
"moji": "🎋"
},
@@ -26440,7 +29706,7 @@
"unicode_alternates": [],
"name": "tangerine",
"shortname": ":tangerine:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26460,7 +29726,7 @@
],
"name": "taurus",
"shortname": ":taurus:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26473,9 +29739,8 @@
"constellation",
"stars",
"zodiac",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♉"
},
@@ -26484,7 +29749,7 @@
"unicode_alternates": [],
"name": "taxi",
"shortname": ":taxi:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26497,7 +29762,8 @@
"automobile",
"city",
"transport",
- "service"
+ "service",
+ "travel"
],
"moji": "🚕"
},
@@ -26506,7 +29772,7 @@
"unicode_alternates": [],
"name": "teacup without handle",
"shortname": ":tea:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26517,10 +29783,13 @@
"green",
"tea",
"leaf",
- "drink",
"teacup",
"hot",
- "beverage"
+ "beverage",
+ "japan",
+ "caffeine",
+ "steam",
+ "morning"
],
"moji": "🍵"
},
@@ -26537,26 +29806,12 @@
"keywords": [
"communication",
"dial",
- "technology"
+ "technology",
+ "electronics",
+ "phone"
],
"moji": "☎"
},
- "telephone_black": {
- "unicode": "1F57F",
- "unicode_alternates": [],
- "name": "black touchtone telephone",
- "shortname": ":telephone_black:",
- "category": "objects_symbols",
- "aliases": [
- ":black_touchtone_telephone:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "dial",
- "technology"
- ]
- },
"telephone_receiver": {
"unicode": "1F4DE",
"unicode_alternates": [],
@@ -26568,26 +29823,12 @@
"keywords": [
"communication",
"dial",
- "technology"
+ "technology",
+ "electronics",
+ "phone"
],
"moji": "📞"
},
- "telephone_white": {
- "unicode": "1F57E",
- "unicode_alternates": [],
- "name": "white touchtone telephone",
- "shortname": ":telephone_white:",
- "category": "objects_symbols",
- "aliases": [
- ":white_touchtone_telephone:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "communication",
- "dial",
- "technology"
- ]
- },
"telescope": {
"unicode": "1F52D",
"unicode_alternates": [],
@@ -26598,13 +29839,15 @@
"aliases_ascii": [],
"keywords": [
"space",
- "stars"
+ "stars",
+ "object",
+ "science"
],
"moji": "🔭"
},
"ten": {
"unicode": "1F51F",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "keycap ten",
"shortname": ":ten:",
"category": "symbols",
@@ -26615,15 +29858,18 @@
"blue-square",
"numbers",
"symbol",
- "word"
- ]
+ "word",
+ "number",
+ "math"
+ ],
+ "moji": "🔟"
},
"tennis": {
"unicode": "1F3BE",
"unicode_alternates": [],
"name": "tennis racquet and ball",
"shortname": ":tennis:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26637,7 +29883,8 @@
"game",
"net",
"court",
- "love"
+ "love",
+ "sport"
],
"moji": "🎾"
},
@@ -26648,13 +29895,16 @@
],
"name": "tent",
"shortname": ":tent:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"camp",
"outdoors",
- "photo"
+ "photo",
+ "places",
+ "travel",
+ "vacation"
],
"moji": "⛺"
},
@@ -26663,16 +29913,21 @@
"unicode_alternates": [],
"name": "thermometer",
"shortname": ":thermometer:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "temperature"
- ]
+ "temperature",
+ "object",
+ "science",
+ "health",
+ "hot"
+ ],
+ "moji": "🌡"
},
"thermometer_face": {
"unicode": "1F912",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "face with thermometer",
"shortname": ":thermometer_face:",
"category": "people",
@@ -26680,11 +29935,17 @@
":face_with_thermometer:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "smiley",
+ "health",
+ "sick",
+ "emotion"
+ ],
+ "moji": "🤒"
},
"thinking": {
"unicode": "1F914",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thinking face",
"shortname": ":thinking:",
"category": "people",
@@ -26692,14 +29953,32 @@
":thinking_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "smiley",
+ "thinking",
+ "boys night"
+ ],
+ "moji": "🤔"
+ },
+ "third_place": {
+ "unicode": "1F949",
+ "unicode_alternates": [],
+ "name": "third place medal",
+ "shortname": ":third_place:",
+ "category": "activity",
+ "aliases": [
+ ":third_place_medal:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥉"
},
"thought_balloon": {
"unicode": "1F4AD",
"unicode_alternates": [],
"name": "thought balloon",
"shortname": ":thought_balloon:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -26711,98 +29990,30 @@
"comic",
"think",
"day dream",
- "wonder"
+ "wonder",
+ "symbol"
],
"moji": "💭"
},
- "thought_left": {
- "unicode": "1F5EC",
- "unicode_alternates": [],
- "name": "left thought bubble",
- "shortname": ":thought_left:",
- "category": "objects_symbols",
- "aliases": [
- ":left_thought_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "cloud",
- "comic",
- "think",
- "day dream",
- "wonder"
- ]
- },
- "thought_right": {
- "unicode": "1F5ED",
- "unicode_alternates": [],
- "name": "right thought bubble",
- "shortname": ":thought_right:",
- "category": "objects_symbols",
- "aliases": [
- ":right_thought_bubble:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "balloon",
- "cloud",
- "comic",
- "think",
- "day dream",
- "wonder"
- ]
- },
"three": {
"moji": "3️⃣",
"unicode": "0033-20E3",
"unicode_alternates": [
"0033-FE0F-20E3"
],
- "name": "digit three",
+ "name": "keycap digit three",
"shortname": ":three:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"3",
"blue-square",
"numbers",
- "prime"
- ]
- },
- "thumbs_down_reverse": {
- "unicode": "1F593",
- "unicode_alternates": [],
- "name": "reversed thumbs down sign",
- "shortname": ":thumbs_down_reverse:",
- "category": "people",
- "aliases": [
- ":reversed_thumbs_down_sign:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "hand",
- "no",
- "-1"
- ]
- },
- "thumbs_up_reverse": {
- "unicode": "1F592",
- "unicode_alternates": [],
- "name": "reversed thumbs up sign",
- "shortname": ":thumbs_up_reverse:",
- "category": "people",
- "aliases": [
- ":reversed_thumbs_up_sign:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "cool",
- "hand",
- "like",
- "yes",
- "+1"
+ "prime",
+ "number",
+ "math",
+ "symbol"
]
},
"thumbsdown": {
@@ -26810,20 +30021,23 @@
"unicode_alternates": [],
"name": "thumbs down sign",
"shortname": ":thumbsdown:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":-1:"
],
"aliases_ascii": [],
"keywords": [
"hand",
- "no"
+ "no",
+ "body",
+ "hands",
+ "diversity"
],
"moji": "👎"
},
"thumbsdown_tone1": {
"unicode": "1F44E-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs down sign tone 1",
"shortname": ":thumbsdown_tone1:",
"category": "people",
@@ -26835,11 +30049,12 @@
"hand",
"no",
"-1"
- ]
+ ],
+ "moji": "👎🏻"
},
"thumbsdown_tone2": {
"unicode": "1F44E-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs down sign tone 2",
"shortname": ":thumbsdown_tone2:",
"category": "people",
@@ -26851,11 +30066,12 @@
"hand",
"no",
"-1"
- ]
+ ],
+ "moji": "👎🏼"
},
"thumbsdown_tone3": {
"unicode": "1F44E-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs down sign tone 3",
"shortname": ":thumbsdown_tone3:",
"category": "people",
@@ -26867,11 +30083,12 @@
"hand",
"no",
"-1"
- ]
+ ],
+ "moji": "👎🏽"
},
"thumbsdown_tone4": {
"unicode": "1F44E-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs down sign tone 4",
"shortname": ":thumbsdown_tone4:",
"category": "people",
@@ -26883,11 +30100,12 @@
"hand",
"no",
"-1"
- ]
+ ],
+ "moji": "👎🏾"
},
"thumbsdown_tone5": {
"unicode": "1F44E-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs down sign tone 5",
"shortname": ":thumbsdown_tone5:",
"category": "people",
@@ -26899,14 +30117,15 @@
"hand",
"no",
"-1"
- ]
+ ],
+ "moji": "👎🏿"
},
"thumbsup": {
"unicode": "1F44D",
"unicode_alternates": [],
"name": "thumbs up sign",
"shortname": ":thumbsup:",
- "category": "emoticons",
+ "category": "people",
"aliases": [
":+1:"
],
@@ -26915,13 +30134,22 @@
"cool",
"hand",
"like",
- "yes"
+ "yes",
+ "body",
+ "hands",
+ "hi",
+ "luck",
+ "thank you",
+ "diversity",
+ "perfect",
+ "good",
+ "beautiful"
],
"moji": "👍"
},
"thumbsup_tone1": {
"unicode": "1F44D-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs up sign tone 1",
"shortname": ":thumbsup_tone1:",
"category": "people",
@@ -26935,11 +30163,12 @@
"like",
"yes",
"+1"
- ]
+ ],
+ "moji": "👍🏻"
},
"thumbsup_tone2": {
"unicode": "1F44D-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs up sign tone 2",
"shortname": ":thumbsup_tone2:",
"category": "people",
@@ -26953,11 +30182,12 @@
"like",
"yes",
"+1"
- ]
+ ],
+ "moji": "👍🏼"
},
"thumbsup_tone3": {
"unicode": "1F44D-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs up sign tone 3",
"shortname": ":thumbsup_tone3:",
"category": "people",
@@ -26971,11 +30201,12 @@
"like",
"yes",
"+1"
- ]
+ ],
+ "moji": "👍🏽"
},
"thumbsup_tone4": {
"unicode": "1F44D-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs up sign tone 4",
"shortname": ":thumbsup_tone4:",
"category": "people",
@@ -26989,11 +30220,12 @@
"like",
"yes",
"+1"
- ]
+ ],
+ "moji": "👍🏾"
},
"thumbsup_tone5": {
"unicode": "1F44D-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thumbs up sign tone 5",
"shortname": ":thumbsup_tone5:",
"category": "people",
@@ -27007,11 +30239,12 @@
"like",
"yes",
"+1"
- ]
+ ],
+ "moji": "👍🏿"
},
"thunder_cloud_rain": {
"unicode": "26C8",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "thunder cloud and rain",
"shortname": ":thunder_cloud_rain:",
"category": "nature",
@@ -27021,15 +30254,20 @@
"aliases_ascii": [],
"keywords": [
"nature",
- "weather"
- ]
+ "weather",
+ "sky",
+ "cloud",
+ "cold",
+ "rain"
+ ],
+ "moji": "⛈"
},
"ticket": {
"unicode": "1F3AB",
"unicode_alternates": [],
"name": "ticket",
"shortname": ":ticket:",
- "category": "places",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27042,7 +30280,10 @@
"stub",
"admission",
"proof",
- "purchase"
+ "purchase",
+ "theatre",
+ "movie",
+ "parties"
],
"moji": "🎫"
},
@@ -27064,8 +30305,12 @@
"entertainment",
"stub",
"proof",
- "purchase"
- ]
+ "purchase",
+ "theatre",
+ "movie",
+ "parties"
+ ],
+ "moji": "🎟"
},
"tiger": {
"unicode": "1F42F",
@@ -27076,7 +30321,10 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "animal"
+ "animal",
+ "wildlife",
+ "roar",
+ "cat"
],
"moji": "🐯"
},
@@ -27096,13 +30344,15 @@
"striped",
"tony",
"tigger",
- "hobs"
+ "hobs",
+ "wildlife",
+ "roar"
],
"moji": "🐅"
},
"timer": {
"unicode": "23F2",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "timer clock",
"shortname": ":timer:",
"category": "objects",
@@ -27113,14 +30363,15 @@
"keywords": [
"object",
"time"
- ]
+ ],
+ "moji": "⏲"
},
"tired_face": {
"unicode": "1F62B",
"unicode_alternates": [],
"name": "tired face",
"shortname": ":tired_face:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27131,13 +30382,18 @@
"whine",
"exhausted",
"sleepy",
- "tired"
+ "tired",
+ "sad",
+ "smiley",
+ "emotion"
],
"moji": "😫"
},
"tm": {
"unicode": "2122",
- "unicode_alternates": "2122-fe0f",
+ "unicode_alternates": [
+ "2122-FE0F"
+ ],
"name": "trade mark sign",
"shortname": ":tm:",
"category": "symbols",
@@ -27149,7 +30405,8 @@
"symbol",
"tm",
"word"
- ]
+ ],
+ "moji": "™"
},
"toilet": {
"unicode": "1F6BD",
@@ -27168,7 +30425,8 @@
"porcelain",
"waste",
"flush",
- "plumbing"
+ "plumbing",
+ "object"
],
"moji": "🚽"
},
@@ -27177,12 +30435,16 @@
"unicode_alternates": [],
"name": "tokyo tower",
"shortname": ":tokyo_tower:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"japan",
- "photo"
+ "photo",
+ "places",
+ "travel",
+ "vacation",
+ "eiffel tower"
],
"moji": "🗼"
},
@@ -27191,7 +30453,7 @@
"unicode_alternates": [],
"name": "tomato",
"shortname": ":tomato:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27200,80 +30462,83 @@
"nature",
"vegetable",
"tomato",
- "fruit",
"sauce",
- "italian"
+ "italian",
+ "vegetables"
],
"moji": "🍅"
},
"tone1": {
"unicode": "1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "emoji modifier Fitzpatrick type-1-2",
"shortname": ":tone1:",
"category": "modifier",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [],
+ "moji": "🏻"
},
"tone2": {
"unicode": "1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "emoji modifier Fitzpatrick type-3",
"shortname": ":tone2:",
"category": "modifier",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [],
+ "moji": "🏼"
},
"tone3": {
"unicode": "1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "emoji modifier Fitzpatrick type-4",
"shortname": ":tone3:",
"category": "modifier",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [],
+ "moji": "🏽"
},
"tone4": {
"unicode": "1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "emoji modifier Fitzpatrick type-5",
"shortname": ":tone4:",
"category": "modifier",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [],
+ "moji": "🏾"
},
"tone5": {
"unicode": "1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "emoji modifier Fitzpatrick type-6",
"shortname": ":tone5:",
"category": "modifier",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [],
+ "moji": "🏿"
},
"tongue": {
"unicode": "1F445",
"unicode_alternates": [],
"name": "tongue",
"shortname": ":tongue:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"mouth",
"playful",
"tongue",
- "mouth",
"taste",
"buds",
"food",
"silly",
- "playful",
"tease",
"kiss",
"french kiss",
@@ -27281,7 +30546,10 @@
"tasty",
"playfulness",
"silliness",
- "intimacy"
+ "intimacy",
+ "body",
+ "sexy",
+ "lip"
],
"moji": "👅"
},
@@ -27290,26 +30558,31 @@
"unicode_alternates": [],
"name": "hammer and wrench",
"shortname": ":tools:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [
":hammer_and_wrench:"
],
"aliases_ascii": [],
"keywords": [
- "tools"
- ]
+ "tools",
+ "object",
+ "tool"
+ ],
+ "moji": "🛠"
},
"top": {
"unicode": "1F51D",
"unicode_alternates": [],
"name": "top with upwards arrow above",
"shortname": ":top:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "words"
+ "words",
+ "arrow",
+ "symbol"
],
"moji": "🔝"
},
@@ -27318,7 +30591,7 @@
"unicode_alternates": [],
"name": "top hat",
"shortname": ":tophat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27337,14 +30610,15 @@
"topper",
"london",
"period piece",
- "magic",
- "magician"
+ "magician",
+ "fashion",
+ "accessories"
],
"moji": "🎩"
},
"track_next": {
"unicode": "23ED",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "black right-pointing double triangle with vertical bar",
"shortname": ":track_next:",
"category": "symbols",
@@ -27358,11 +30632,12 @@
"next track",
"sound",
"symbol"
- ]
+ ],
+ "moji": "⏭"
},
"track_previous": {
"unicode": "23EE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "black left-pointing double triangle with vertical bar",
"shortname": ":track_previous:",
"category": "symbols",
@@ -27376,28 +30651,34 @@
"previous track",
"sound",
"symbol"
- ]
+ ],
+ "moji": "⏮"
},
"trackball": {
"unicode": "1F5B2",
"unicode_alternates": [],
"name": "trackball",
"shortname": ":trackball:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"input",
"device",
- "gadget"
- ]
+ "gadget",
+ "electronics",
+ "work",
+ "game",
+ "office"
+ ],
+ "moji": "🖲"
},
"tractor": {
"unicode": "1F69C",
"unicode_alternates": [],
"name": "tractor",
"shortname": ":tractor:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27409,7 +30690,8 @@
"farm",
"construction",
"machine",
- "digger"
+ "digger",
+ "transportation"
],
"moji": "🚜"
},
@@ -27418,18 +30700,19 @@
"unicode_alternates": [],
"name": "horizontal traffic light",
"shortname": ":traffic_light:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"traffic",
"transportation",
- "traffic",
"light",
"stop",
"go",
"yield",
- "horizontal"
+ "horizontal",
+ "object",
+ "stop light"
],
"moji": "🚥"
},
@@ -27438,20 +30721,24 @@
"unicode_alternates": [],
"name": "Tram Car",
"shortname": ":train:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"tram",
- "rail"
- ]
+ "rail",
+ "transportation",
+ "travel",
+ "train"
+ ],
+ "moji": "🚋"
},
"train2": {
"unicode": "1F686",
"unicode_alternates": [],
"name": "train",
"shortname": ":train2:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27459,66 +30746,35 @@
"vehicle",
"train",
"locomotive",
- "rail"
+ "rail",
+ "travel"
],
"moji": "🚆"
},
- "train_diesel": {
- "unicode": "1F6F2",
- "unicode_alternates": [],
- "name": "diesel locomotive",
- "shortname": ":train_diesel:",
- "category": "travel_places",
- "aliases": [
- ":diesel_locomotive:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "train",
- "transportation",
- "engine",
- "rail"
- ]
- },
"tram": {
"unicode": "1F68A",
"unicode_alternates": [],
"name": "tram",
"shortname": ":tram:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"transportation",
"vehicle",
"tram",
- "transportation",
- "transport"
+ "transport",
+ "travel",
+ "train"
],
"moji": "🚊"
},
- "triangle_round": {
- "unicode": "1F6C6",
- "unicode_alternates": [],
- "name": "triangle with rounded corners",
- "shortname": ":triangle_round:",
- "category": "objects_symbols",
- "aliases": [
- ":triangle_with_rounded_corners:"
- ],
- "aliases_ascii": [],
- "keywords": [
- "caution",
- "warning",
- "alert"
- ]
- },
"triangular_flag_on_post": {
"unicode": "1F6A9",
"unicode_alternates": [],
"name": "triangular flag on post",
"shortname": ":triangular_flag_on_post:",
- "category": "places",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27527,7 +30783,8 @@
"flag",
"golf",
"post",
- "flagpole"
+ "flagpole",
+ "object"
],
"moji": "🚩"
},
@@ -27543,7 +30800,10 @@
"architect",
"math",
"sketch",
- "stationery"
+ "stationery",
+ "object",
+ "tool",
+ "office"
],
"moji": "📐"
},
@@ -27552,12 +30812,14 @@
"unicode_alternates": [],
"name": "trident emblem",
"shortname": ":trident:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"spear",
- "weapon"
+ "weapon",
+ "object",
+ "symbol"
],
"moji": "🔱"
},
@@ -27566,7 +30828,7 @@
"unicode_alternates": [],
"name": "face with look of triumph",
"shortname": ":triumph:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27575,7 +30837,11 @@
"phew",
"triumph",
"steam",
- "breath"
+ "breath",
+ "mad",
+ "smiley",
+ "angry",
+ "emotion"
],
"moji": "😤"
},
@@ -27584,7 +30850,7 @@
"unicode_alternates": [],
"name": "trolleybus",
"shortname": ":trolleybus:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27595,7 +30861,7 @@
"bus",
"city",
"transport",
- "transportation"
+ "travel"
],
"moji": "🚎"
},
@@ -27604,7 +30870,7 @@
"unicode_alternates": [],
"name": "trophy",
"shortname": ":trophy:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27617,11 +30883,13 @@
"trophy",
"first",
"show",
- "place",
- "win",
"reward",
"achievement",
- "medal"
+ "medal",
+ "object",
+ "game",
+ "perfect",
+ "parties"
],
"moji": "🏆"
},
@@ -27630,7 +30898,7 @@
"unicode_alternates": [],
"name": "tropical drink",
"shortname": ":tropical_drink:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27642,7 +30910,9 @@
"coconut",
"pina",
"fruit",
- "umbrella"
+ "umbrella",
+ "cocktail",
+ "alcohol"
],
"moji": "🍹"
},
@@ -27656,7 +30926,8 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "swim"
+ "swim",
+ "wildlife"
],
"moji": "🐠"
},
@@ -27665,7 +30936,7 @@
"unicode_alternates": [],
"name": "delivery truck",
"shortname": ":truck:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27682,16 +30953,15 @@
"unicode_alternates": [],
"name": "trumpet",
"shortname": ":trumpet:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"brass",
"music",
"trumpet",
- "brass",
- "music",
- "instrument"
+ "instrument",
+ "instruments"
],
"moji": "🎺"
},
@@ -27711,34 +30981,40 @@
"flower",
"bulb",
"spring",
- "easter"
+ "easter",
+ "vagina",
+ "girls night"
],
"moji": "🌷"
},
+ "tumbler_glass": {
+ "unicode": "1F943",
+ "unicode_alternates": [],
+ "name": "tumbler glass",
+ "shortname": ":tumbler_glass:",
+ "category": "food",
+ "aliases": [
+ ":whisky:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [
+ "booze"
+ ],
+ "moji": "🥃"
+ },
"turkey": {
"unicode": "1F983",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "turkey",
"shortname": ":turkey:",
"category": "nature",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
- },
- "turned_ok_hand": {
- "unicode": "1F58F",
- "unicode_alternates": [],
- "name": "turned ok hand sign",
- "shortname": ":turned_ok_hand:",
- "category": "people",
- "aliases": [
- ":turned_ok_hand_sign:"
- ],
- "aliases_ascii": [],
"keywords": [
- "perfect",
- "okay"
- ]
+ "wildlife",
+ "animal"
+ ],
+ "moji": "🦃"
},
"turtle": {
"unicode": "1F422",
@@ -27756,15 +31032,15 @@
"tortoise",
"chelonian",
"reptile",
- "slow",
"snap",
- "steady"
+ "steady",
+ "wildlife"
],
"moji": "🐢"
},
"tv": {
"unicode": "1F4FA",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "television",
"shortname": ":tv:",
"category": "objects",
@@ -27778,19 +31054,23 @@
"tv",
"entertainment",
"object",
- "video"
- ]
+ "video",
+ "electronics"
+ ],
+ "moji": "📺"
},
"twisted_rightwards_arrows": {
"unicode": "1F500",
"unicode_alternates": [],
"name": "twisted rightwards arrows",
"shortname": ":twisted_rightwards_arrows:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "arrow",
+ "symbol"
],
"moji": "🔀"
},
@@ -27800,16 +31080,19 @@
"unicode_alternates": [
"0032-FE0F-20E3"
],
- "name": "digit two",
+ "name": "keycap digit two",
"shortname": ":two:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"2",
"blue-square",
"numbers",
- "prime"
+ "prime",
+ "number",
+ "math",
+ "symbol"
]
},
"two_hearts": {
@@ -27817,7 +31100,7 @@
"unicode_alternates": [],
"name": "two hearts",
"shortname": ":two_hearts:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27828,8 +31111,8 @@
"heart",
"hearts",
"two",
- "love",
- "emotion"
+ "emotion",
+ "symbol"
],
"moji": "💕"
},
@@ -27838,7 +31121,7 @@
"unicode_alternates": [],
"name": "two men holding hands",
"shortname": ":two_men_holding_hands:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27850,11 +31133,13 @@
"men",
"gay",
"homosexual",
- "friends",
"hands",
"holding",
"team",
- "unity"
+ "unity",
+ "people",
+ "sex",
+ "lgbt"
],
"moji": "👬"
},
@@ -27863,7 +31148,7 @@
"unicode_alternates": [],
"name": "two women holding hands",
"shortname": ":two_women_holding_hands:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27875,14 +31160,17 @@
"women",
"hands",
"girlfriends",
- "friends",
"sisters",
"mother",
"daughter",
"gay",
"homosexual",
- "couple",
- "unity"
+ "unity",
+ "people",
+ "sex",
+ "lgbt",
+ "lesbian",
+ "girls night"
],
"moji": "👭"
},
@@ -27891,7 +31179,7 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-5272",
"shortname": ":u5272:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27899,7 +31187,8 @@
"cut",
"divide",
"kanji",
- "pink"
+ "pink",
+ "symbol"
],
"moji": "🈹"
},
@@ -27908,14 +31197,16 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-5408",
"shortname": ":u5408:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"japanese",
"join",
- "kanji"
+ "kanji",
+ "japan",
+ "symbol"
],
"moji": "🈴"
},
@@ -27924,12 +31215,13 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-55b6",
"shortname": ":u55b6:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"japanese",
- "opening hours"
+ "opening hours",
+ "symbol"
],
"moji": "🈺"
},
@@ -27940,14 +31232,15 @@
],
"name": "squared cjk unified ideograph-6307",
"shortname": ":u6307:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"green-square",
"kanji",
- "point"
+ "point",
+ "symbol"
],
"moji": "🈯"
},
@@ -27956,7 +31249,7 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-6708",
"shortname": ":u6708:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27964,7 +31257,8 @@
"japanese",
"kanji",
"moon",
- "orange-square"
+ "orange-square",
+ "symbol"
],
"moji": "🈷"
},
@@ -27973,14 +31267,15 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-6709",
"shortname": ":u6709:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"have",
"kanji",
- "orange-square"
+ "orange-square",
+ "symbol"
],
"moji": "🈶"
},
@@ -27989,7 +31284,7 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-6e80",
"shortname": ":u6e80:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -27997,7 +31292,9 @@
"full",
"japanese",
"kanji",
- "red-square"
+ "red-square",
+ "japan",
+ "symbol"
],
"moji": "🈵"
},
@@ -28008,7 +31305,7 @@
],
"name": "squared cjk unified ideograph-7121",
"shortname": ":u7121:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28017,7 +31314,8 @@
"kanji",
"no",
"nothing",
- "orange-square"
+ "orange-square",
+ "symbol"
],
"moji": "🈚"
},
@@ -28026,13 +31324,14 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-7533",
"shortname": ":u7533:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"japanese",
- "kanji"
+ "kanji",
+ "symbol"
],
"moji": "🈸"
},
@@ -28041,7 +31340,7 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-7981",
"shortname": ":u7981:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28050,7 +31349,9 @@
"japanese",
"kanji",
"limit",
- "restricted"
+ "restricted",
+ "japan",
+ "symbol"
],
"moji": "🈲"
},
@@ -28059,14 +31360,15 @@
"unicode_alternates": [],
"name": "squared cjk unified ideograph-7a7a",
"shortname": ":u7a7a:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"chinese",
"empty",
"japanese",
- "kanji"
+ "kanji",
+ "symbol"
],
"moji": "🈳"
},
@@ -28082,13 +31384,15 @@
"aliases_ascii": [],
"keywords": [
"rain",
- "weather"
+ "weather",
+ "sky",
+ "cold"
],
"moji": "☔"
},
"umbrella2": {
"unicode": "2602",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "umbrella",
"shortname": ":umbrella2:",
"category": "nature",
@@ -28098,15 +31402,19 @@
"clothing",
"nature",
"rain",
- "weather"
- ]
+ "weather",
+ "object",
+ "sky",
+ "cold"
+ ],
+ "moji": "☂"
},
"unamused": {
"unicode": "1F612",
"unicode_alternates": [],
"name": "unamused face",
"shortname": ":unamused:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28120,7 +31428,12 @@
"depressed",
"unhappy",
"disapprove",
- "lame"
+ "lame",
+ "sad",
+ "mad",
+ "smiley",
+ "tired",
+ "emotion"
],
"moji": "😒"
},
@@ -28129,20 +31442,21 @@
"unicode_alternates": [],
"name": "no one under eighteen symbol",
"shortname": ":underage:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"18",
"drink",
"night",
- "pub"
+ "pub",
+ "symbol"
],
"moji": "🔞"
},
"unicorn": {
"unicode": "1F984",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "unicorn face",
"shortname": ":unicorn:",
"category": "nature",
@@ -28150,7 +31464,10 @@
":unicorn_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "animal"
+ ],
+ "moji": "🦄"
},
"unlock": {
"unicode": "1F513",
@@ -28162,7 +31479,9 @@
"aliases_ascii": [],
"keywords": [
"privacy",
- "security"
+ "security",
+ "object",
+ "lock"
],
"moji": "🔓"
},
@@ -28171,17 +31490,18 @@
"unicode_alternates": [],
"name": "squared up with exclamation mark",
"shortname": ":up:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "blue-square"
+ "blue-square",
+ "symbol"
],
"moji": "🆙"
},
"upside_down": {
"unicode": "1F643",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "upside-down face",
"shortname": ":upside_down:",
"category": "people",
@@ -28189,11 +31509,16 @@
":upside_down_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "silly",
+ "smiley",
+ "sarcastic"
+ ],
+ "moji": "🙃"
},
"urn": {
"unicode": "26B1",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "funeral urn",
"shortname": ":urn:",
"category": "objects",
@@ -28203,8 +31528,11 @@
"aliases_ascii": [],
"keywords": [
"death",
- "object"
- ]
+ "object",
+ "dead",
+ "rip"
+ ],
+ "moji": "⚱"
},
"v": {
"unicode": "270C",
@@ -28213,7 +31541,7 @@
],
"name": "victory hand",
"shortname": ":v:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28222,13 +31550,19 @@
"ohyeah",
"peace",
"two",
- "victory"
+ "victory",
+ "body",
+ "hands",
+ "hi",
+ "thank you",
+ "diversity",
+ "girls night"
],
"moji": "✌"
},
"v_tone1": {
"unicode": "270C-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "victory hand tone 1",
"shortname": ":v_tone1:",
"category": "people",
@@ -28240,11 +31574,12 @@
"peace",
"two",
"v"
- ]
+ ],
+ "moji": "✌🏻"
},
"v_tone2": {
"unicode": "270C-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "victory hand tone 2",
"shortname": ":v_tone2:",
"category": "people",
@@ -28256,11 +31591,12 @@
"peace",
"two",
"v"
- ]
+ ],
+ "moji": "✌🏼"
},
"v_tone3": {
"unicode": "270C-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "victory hand tone 3",
"shortname": ":v_tone3:",
"category": "people",
@@ -28272,11 +31608,12 @@
"peace",
"two",
"v"
- ]
+ ],
+ "moji": "✌🏽"
},
"v_tone4": {
"unicode": "270C-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "victory hand tone 4",
"shortname": ":v_tone4:",
"category": "people",
@@ -28288,11 +31625,12 @@
"peace",
"two",
"v"
- ]
+ ],
+ "moji": "✌🏾"
},
"v_tone5": {
"unicode": "270C-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "victory hand tone 5",
"shortname": ":v_tone5:",
"category": "people",
@@ -28304,14 +31642,15 @@
"peace",
"two",
"v"
- ]
+ ],
+ "moji": "✌🏿"
},
"vertical_traffic_light": {
"unicode": "1F6A6",
"unicode_alternates": [],
"name": "vertical traffic light",
"shortname": ":vertical_traffic_light:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28321,7 +31660,9 @@
"stop",
"go",
"yield",
- "vertical"
+ "vertical",
+ "object",
+ "stop light"
],
"moji": "🚦"
},
@@ -28336,7 +31677,8 @@
"keywords": [
"oldschool",
"record",
- "video"
+ "video",
+ "electronics"
],
"moji": "📼"
},
@@ -28345,12 +31687,13 @@
"unicode_alternates": [],
"name": "vibration mode",
"shortname": ":vibration_mode:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"orange-square",
- "phone"
+ "phone",
+ "symbol"
],
"moji": "📳"
},
@@ -28364,7 +31707,10 @@
"aliases_ascii": [],
"keywords": [
"film",
- "record"
+ "record",
+ "electronics",
+ "camera",
+ "movie"
],
"moji": "📹"
},
@@ -28373,7 +31719,7 @@
"unicode_alternates": [],
"name": "video game",
"shortname": ":video_game:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28383,11 +31729,11 @@
"play",
"video",
"game",
- "console",
- "controller",
"nintendo",
"xbox",
- "playstation"
+ "playstation",
+ "electronics",
+ "boys night"
],
"moji": "🎮"
},
@@ -28396,7 +31742,7 @@
"unicode_alternates": [],
"name": "violin",
"shortname": ":violin:",
- "category": "objects",
+ "category": "activity",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28404,8 +31750,8 @@
"music",
"violin",
"fiddle",
- "music",
- "instrument"
+ "instruments",
+ "sarcastic"
],
"moji": "🎻"
},
@@ -28416,7 +31762,7 @@
],
"name": "virgo",
"shortname": ":virgo:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28428,9 +31774,8 @@
"constellation",
"stars",
"zodiac",
- "sign",
- "zodiac",
- "horoscope"
+ "horoscope",
+ "symbol"
],
"moji": "♍"
},
@@ -28439,7 +31784,7 @@
"unicode_alternates": [],
"name": "volcano",
"shortname": ":volcano:",
- "category": "nature",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28449,31 +31794,40 @@
"lava",
"magma",
"hot",
- "explode"
+ "explode",
+ "places",
+ "tropical"
],
"moji": "🌋"
},
"volleyball": {
"unicode": "1F3D0",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "volleyball",
"shortname": ":volleyball:",
"category": "activity",
"aliases": [],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "game",
+ "ball",
+ "sport",
+ "volleyball"
+ ],
+ "moji": "🏐"
},
"vs": {
"unicode": "1F19A",
"unicode_alternates": [],
"name": "squared vs",
"shortname": ":vs:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"orange-square",
- "words"
+ "words",
+ "symbol"
],
"moji": "🆚"
},
@@ -28493,12 +31847,17 @@
"leonard",
"nimoy",
"star trek",
- "live long"
- ]
+ "live long",
+ "body",
+ "hands",
+ "hi",
+ "diversity"
+ ],
+ "moji": "🖖"
},
"vulcan_tone1": {
"unicode": "1F596-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with part between middle and ring fingers tone 1",
"shortname": ":vulcan_tone1:",
"category": "people",
@@ -28513,11 +31872,12 @@
"nimoy",
"star trek",
"live long"
- ]
+ ],
+ "moji": "🖖🏻"
},
"vulcan_tone2": {
"unicode": "1F596-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with part between middle and ring fingers tone 2",
"shortname": ":vulcan_tone2:",
"category": "people",
@@ -28532,11 +31892,12 @@
"nimoy",
"star trek",
"live long"
- ]
+ ],
+ "moji": "🖖🏼"
},
"vulcan_tone3": {
"unicode": "1F596-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with part between middle and ring fingers tone 3",
"shortname": ":vulcan_tone3:",
"category": "people",
@@ -28551,11 +31912,12 @@
"nimoy",
"star trek",
"live long"
- ]
+ ],
+ "moji": "🖖🏽"
},
"vulcan_tone4": {
"unicode": "1F596-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with part between middle and ring fingers tone 4",
"shortname": ":vulcan_tone4:",
"category": "people",
@@ -28570,11 +31932,12 @@
"nimoy",
"star trek",
"live long"
- ]
+ ],
+ "moji": "🖖🏾"
},
"vulcan_tone5": {
"unicode": "1F596-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "raised hand with part between middle and ring fingers tone 5",
"shortname": ":vulcan_tone5:",
"category": "people",
@@ -28589,14 +31952,15 @@
"nimoy",
"star trek",
"live long"
- ]
+ ],
+ "moji": "🖖🏿"
},
"walking": {
"unicode": "1F6B6",
"unicode_alternates": [],
"name": "pedestrian",
"shortname": ":walking:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28607,13 +31971,16 @@
"stroll",
"stride",
"foot",
- "feet"
+ "feet",
+ "people",
+ "men",
+ "diversity"
],
"moji": "🚶"
},
"walking_tone1": {
"unicode": "1F6B6-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pedestrian tone 1",
"shortname": ":walking_tone1:",
"category": "people",
@@ -28626,11 +31993,12 @@
"stride",
"hiking",
"hike"
- ]
+ ],
+ "moji": "🚶🏻"
},
"walking_tone2": {
"unicode": "1F6B6-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pedestrian tone 2",
"shortname": ":walking_tone2:",
"category": "people",
@@ -28643,11 +32011,12 @@
"stride",
"hiking",
"hike"
- ]
+ ],
+ "moji": "🚶🏼"
},
"walking_tone3": {
"unicode": "1F6B6-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pedestrian tone 3",
"shortname": ":walking_tone3:",
"category": "people",
@@ -28660,11 +32029,12 @@
"stride",
"hiking",
"hike"
- ]
+ ],
+ "moji": "🚶🏽"
},
"walking_tone4": {
"unicode": "1F6B6-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pedestrian tone 4",
"shortname": ":walking_tone4:",
"category": "people",
@@ -28677,11 +32047,12 @@
"stride",
"hiking",
"hike"
- ]
+ ],
+ "moji": "🚶🏾"
},
"walking_tone5": {
"unicode": "1F6B6-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "pedestrian tone 5",
"shortname": ":walking_tone5:",
"category": "people",
@@ -28694,7 +32065,8 @@
"stride",
"hiking",
"hike"
- ]
+ ],
+ "moji": "🚶🏿"
},
"waning_crescent_moon": {
"unicode": "1F318",
@@ -28712,7 +32084,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌘"
},
@@ -28732,7 +32105,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌖"
},
@@ -28743,12 +32117,14 @@
],
"name": "warning sign",
"shortname": ":warning:",
- "category": "places",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"exclamation",
- "wip"
+ "wip",
+ "symbol",
+ "punctuation"
],
"moji": "⚠"
},
@@ -28757,14 +32133,17 @@
"unicode_alternates": [],
"name": "wastebasket",
"shortname": ":wastebasket:",
- "category": "objects_symbols",
+ "category": "objects",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"trash",
"garbage",
- "dispose"
- ]
+ "dispose",
+ "object",
+ "work"
+ ],
+ "moji": "🗑"
},
"watch": {
"unicode": "231A",
@@ -28778,7 +32157,8 @@
"aliases_ascii": [],
"keywords": [
"accessories",
- "time"
+ "time",
+ "electronics"
],
"moji": "⌚"
},
@@ -28800,16 +32180,83 @@
"asia",
"bovine",
"milk",
- "dairy"
+ "dairy",
+ "wildlife"
],
"moji": "🐃"
},
+ "water_polo": {
+ "unicode": "1F93D",
+ "unicode_alternates": [],
+ "name": "water polo",
+ "shortname": ":water_polo:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽"
+ },
+ "water_polo_tone1": {
+ "unicode": "1F93D-1F3FB",
+ "unicode_alternates": [],
+ "name": "water polo tone 1",
+ "shortname": ":water_polo_tone1:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽🏻"
+ },
+ "water_polo_tone2": {
+ "unicode": "1F93D-1F3FC",
+ "unicode_alternates": [],
+ "name": "water polo tone 2",
+ "shortname": ":water_polo_tone2:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽🏼"
+ },
+ "water_polo_tone3": {
+ "unicode": "1F93D-1F3FD",
+ "unicode_alternates": [],
+ "name": "water polo tone 3",
+ "shortname": ":water_polo_tone3:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽🏽"
+ },
+ "water_polo_tone4": {
+ "unicode": "1F93D-1F3FE",
+ "unicode_alternates": [],
+ "name": "water polo tone 4",
+ "shortname": ":water_polo_tone4:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽🏾"
+ },
+ "water_polo_tone5": {
+ "unicode": "1F93D-1F3FF",
+ "unicode_alternates": [],
+ "name": "water polo tone 5",
+ "shortname": ":water_polo_tone5:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤽🏿"
+ },
"watermelon": {
"unicode": "1F349",
"unicode_alternates": [],
"name": "watermelon",
"shortname": ":watermelon:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28818,7 +32265,6 @@
"melon",
"watermelon",
"summer",
- "fruit",
"large"
],
"moji": "🍉"
@@ -28828,7 +32274,7 @@
"unicode_alternates": [],
"name": "waving hand sign",
"shortname": ":wave:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28836,13 +32282,16 @@
"gesture",
"goodbye",
"hands",
- "solong"
+ "solong",
+ "body",
+ "hi",
+ "diversity"
],
"moji": "👋"
},
"wave_tone1": {
"unicode": "1F44B-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "waving hand sign tone 1",
"shortname": ":wave_tone1:",
"category": "people",
@@ -28855,11 +32304,12 @@
"solong",
"hi",
"wave"
- ]
+ ],
+ "moji": "👋🏻"
},
"wave_tone2": {
"unicode": "1F44B-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "waving hand sign tone 2",
"shortname": ":wave_tone2:",
"category": "people",
@@ -28872,11 +32322,12 @@
"solong",
"hi",
"wave"
- ]
+ ],
+ "moji": "👋🏼"
},
"wave_tone3": {
"unicode": "1F44B-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "waving hand sign tone 3",
"shortname": ":wave_tone3:",
"category": "people",
@@ -28889,11 +32340,12 @@
"solong",
"hi",
"wave"
- ]
+ ],
+ "moji": "👋🏽"
},
"wave_tone4": {
"unicode": "1F44B-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "waving hand sign tone 4",
"shortname": ":wave_tone4:",
"category": "people",
@@ -28906,11 +32358,12 @@
"solong",
"hi",
"wave"
- ]
+ ],
+ "moji": "👋🏾"
},
"wave_tone5": {
"unicode": "1F44B-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "waving hand sign tone 5",
"shortname": ":wave_tone5:",
"category": "people",
@@ -28923,19 +32376,21 @@
"solong",
"hi",
"wave"
- ]
+ ],
+ "moji": "👋🏿"
},
"wavy_dash": {
"unicode": "3030",
"unicode_alternates": [],
"name": "wavy dash",
"shortname": ":wavy_dash:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"draw",
- "line"
+ "line",
+ "symbol"
],
"moji": "〰"
},
@@ -28954,7 +32409,8 @@
"sky",
"night",
"cheese",
- "phase"
+ "phase",
+ "space"
],
"moji": "🌒"
},
@@ -28967,7 +32423,10 @@
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "nature"
+ "nature",
+ "space",
+ "sky",
+ "moon"
],
"moji": "🌔"
},
@@ -28976,7 +32435,7 @@
"unicode_alternates": [],
"name": "water closet",
"shortname": ":wc:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -28985,13 +32444,13 @@
"toilet",
"water",
"closet",
- "toilet",
"bathroom",
"throne",
"porcelain",
"waste",
"flush",
- "plumbing"
+ "plumbing",
+ "symbol"
],
"moji": "🚾"
},
@@ -29000,7 +32459,7 @@
"unicode_alternates": [],
"name": "weary face",
"shortname": ":weary:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29010,13 +32469,14 @@
"sleepy",
"tired",
"weary",
- "sleepy",
- "tired",
"tiredness",
"study",
"finals",
"school",
- "exhausted"
+ "exhausted",
+ "smiley",
+ "stressed",
+ "emotion"
],
"moji": "😩"
},
@@ -29025,7 +32485,7 @@
"unicode_alternates": [],
"name": "wedding",
"shortname": ":wedding:",
- "category": "places",
+ "category": "travel",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29035,7 +32495,11 @@
"groom",
"like",
"love",
- "marriage"
+ "marriage",
+ "places",
+ "wedding",
+ "building",
+ "parties"
],
"moji": "💒"
},
@@ -29051,7 +32515,10 @@
"animal",
"nature",
"ocean",
- "sea"
+ "sea",
+ "wildlife",
+ "tropical",
+ "whales"
],
"moji": "🐳"
},
@@ -29073,13 +32540,16 @@
"bloated",
"fat",
"large",
- "massive"
+ "massive",
+ "wildlife",
+ "tropical",
+ "whales"
],
"moji": "🐋"
},
"wheel_of_dharma": {
"unicode": "2638",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "wheel of dharma",
"shortname": ":wheel_of_dharma:",
"category": "symbols",
@@ -29089,7 +32559,8 @@
"buddhist",
"religion",
"symbol"
- ]
+ ],
+ "moji": "☸"
},
"wheelchair": {
"unicode": "267F",
@@ -29098,12 +32569,13 @@
],
"name": "wheelchair symbol",
"shortname": ":wheelchair:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
- "disabled"
+ "disabled",
+ "symbol"
],
"moji": "♿"
},
@@ -29112,13 +32584,14 @@
"unicode_alternates": [],
"name": "white heavy check mark",
"shortname": ":white_check_mark:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"agree",
"green-square",
- "ok"
+ "ok",
+ "symbol"
],
"moji": "✅"
},
@@ -29129,11 +32602,14 @@
],
"name": "medium white circle",
"shortname": ":white_circle:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "circle"
],
"moji": "⚪"
},
@@ -29142,7 +32618,7 @@
"unicode_alternates": [],
"name": "white flower",
"shortname": ":white_flower:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29158,7 +32634,8 @@
"homework",
"student",
"assignment",
- "praise"
+ "praise",
+ "symbol"
],
"moji": "💮"
},
@@ -29169,11 +32646,14 @@
],
"name": "white large square",
"shortname": ":white_large_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "⬜"
},
@@ -29184,11 +32664,14 @@
],
"name": "white medium small square",
"shortname": ":white_medium_small_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "◽"
},
@@ -29199,11 +32682,14 @@
],
"name": "white medium square",
"shortname": ":white_medium_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "◻"
},
@@ -29214,11 +32700,14 @@
],
"name": "white small square",
"shortname": ":white_small_square:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "▫"
},
@@ -29227,17 +32716,20 @@
"unicode_alternates": [],
"name": "white square button",
"shortname": ":white_square_button:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
- "shape"
+ "shape",
+ "shapes",
+ "symbol",
+ "square"
],
"moji": "🔳"
},
"white_sun_cloud": {
"unicode": "1F325",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white sun behind cloud",
"shortname": ":white_sun_cloud:",
"category": "nature",
@@ -29247,12 +32739,17 @@
"aliases_ascii": [],
"keywords": [
"nature",
- "weather"
- ]
+ "weather",
+ "sky",
+ "cloud",
+ "cold",
+ "sun"
+ ],
+ "moji": "🌥"
},
"white_sun_rain_cloud": {
"unicode": "1F326",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white sun behind cloud with rain",
"shortname": ":white_sun_rain_cloud:",
"category": "nature",
@@ -29262,12 +32759,18 @@
"aliases_ascii": [],
"keywords": [
"nature",
- "weather"
- ]
+ "weather",
+ "sky",
+ "cloud",
+ "cold",
+ "rain",
+ "sun"
+ ],
+ "moji": "🌦"
},
"white_sun_small_cloud": {
"unicode": "1F324",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "white sun with small cloud",
"shortname": ":white_sun_small_cloud:",
"category": "nature",
@@ -29277,8 +32780,25 @@
"aliases_ascii": [],
"keywords": [
"nature",
- "weather"
- ]
+ "weather",
+ "sky",
+ "cloud",
+ "sun"
+ ],
+ "moji": "🌤"
+ },
+ "wilted_rose": {
+ "unicode": "1F940",
+ "unicode_alternates": [],
+ "name": "wilted flower",
+ "shortname": ":wilted_rose:",
+ "category": "nature",
+ "aliases": [
+ ":wilted_flower:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🥀"
},
"wind_blowing_face": {
"unicode": "1F32C",
@@ -29290,8 +32810,11 @@
"aliases_ascii": [],
"keywords": [
"mother",
- "nature"
- ]
+ "nature",
+ "weather",
+ "cold"
+ ],
+ "moji": "🌬"
},
"wind_chime": {
"unicode": "1F390",
@@ -29314,7 +32837,9 @@
"soothing",
"protective",
"spiritual",
- "sound"
+ "sound",
+ "object",
+ "japan"
],
"moji": "🎐"
},
@@ -29323,7 +32848,7 @@
"unicode_alternates": [],
"name": "wine glass",
"shortname": ":wine_glass:",
- "category": "objects",
+ "category": "food",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29338,7 +32863,10 @@
"grapes",
"tasting",
"wine",
- "winery"
+ "winery",
+ "italian",
+ "girls night",
+ "parties"
],
"moji": "🍷"
},
@@ -29347,7 +32875,7 @@
"unicode_alternates": [],
"name": "winking face",
"shortname": ":wink:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [
";)",
@@ -29367,7 +32895,10 @@
"wink",
"winking",
"friendly",
- "joke"
+ "joke",
+ "silly",
+ "smiley",
+ "emotion"
],
"moji": "😉"
},
@@ -29381,7 +32912,9 @@
"aliases_ascii": [],
"keywords": [
"animal",
- "nature"
+ "nature",
+ "wildlife",
+ "roar"
],
"moji": "🐺"
},
@@ -29390,18 +32923,25 @@
"unicode_alternates": [],
"name": "woman",
"shortname": ":woman:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"female",
- "girls"
+ "girls",
+ "people",
+ "women",
+ "sex",
+ "diversity",
+ "feminist",
+ "selfie",
+ "girls night"
],
"moji": "👩"
},
"woman_tone1": {
"unicode": "1F469-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "woman tone 1",
"shortname": ":woman_tone1:",
"category": "people",
@@ -29411,11 +32951,12 @@
"female",
"girl",
"lady"
- ]
+ ],
+ "moji": "👩🏻"
},
"woman_tone2": {
"unicode": "1F469-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "woman tone 2",
"shortname": ":woman_tone2:",
"category": "people",
@@ -29425,11 +32966,12 @@
"female",
"girl",
"lady"
- ]
+ ],
+ "moji": "👩🏼"
},
"woman_tone3": {
"unicode": "1F469-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "woman tone 3",
"shortname": ":woman_tone3:",
"category": "people",
@@ -29439,11 +32981,12 @@
"female",
"girl",
"lady"
- ]
+ ],
+ "moji": "👩🏽"
},
"woman_tone4": {
"unicode": "1F469-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "woman tone 4",
"shortname": ":woman_tone4:",
"category": "people",
@@ -29453,11 +32996,12 @@
"female",
"girl",
"lady"
- ]
+ ],
+ "moji": "👩🏾"
},
"woman_tone5": {
"unicode": "1F469-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "woman tone 5",
"shortname": ":woman_tone5:",
"category": "people",
@@ -29467,14 +33011,15 @@
"female",
"girl",
"lady"
- ]
+ ],
+ "moji": "👩🏿"
},
"womans_clothes": {
"unicode": "1F45A",
"unicode_alternates": [],
"name": "womans clothes",
"shortname": ":womans_clothes:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29490,7 +33035,8 @@
"shopping",
"shop",
"dressing",
- "dressed"
+ "dressed",
+ "women"
],
"moji": "👚"
},
@@ -29499,13 +33045,14 @@
"unicode_alternates": [],
"name": "womans hat",
"shortname": ":womans_hat:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"accessories",
"fashion",
- "female"
+ "female",
+ "women"
],
"moji": "👒"
},
@@ -29514,7 +33061,7 @@
"unicode_alternates": [],
"name": "womens symbol",
"shortname": ":womens:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29525,7 +33072,8 @@
"sign",
"girl",
"female",
- "avatar"
+ "avatar",
+ "symbol"
],
"moji": "🚺"
},
@@ -29534,7 +33082,7 @@
"unicode_alternates": [],
"name": "worried face",
"shortname": ":worried:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29544,8 +33092,10 @@
"worried",
"anxious",
"distressed",
- "nervous",
- "tense"
+ "tense",
+ "sad",
+ "smiley",
+ "emotion"
],
"moji": "😟"
},
@@ -29560,30 +33110,111 @@
"keywords": [
"diy",
"ikea",
- "tools"
+ "tools",
+ "object",
+ "tool"
],
"moji": "🔧"
},
- "writing_hand": {
- "unicode": "1F58E",
+ "wrestlers": {
+ "unicode": "1F93C",
"unicode_alternates": [],
- "name": "left writing hand",
- "shortname": ":writing_hand:",
- "category": "people",
+ "name": "wrestlers",
+ "shortname": ":wrestlers:",
+ "category": "activity",
+ "aliases": [
+ ":wrestling:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼"
+ },
+ "wrestlers_tone1": {
+ "unicode": "1F93C-1F3FB",
+ "unicode_alternates": [],
+ "name": "wrestlers tone 1",
+ "shortname": ":wrestlers_tone1:",
+ "category": "activity",
+ "aliases": [
+ ":wrestling_tone1:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼🏻"
+ },
+ "wrestlers_tone2": {
+ "unicode": "1F93C-1F3FC",
+ "unicode_alternates": [],
+ "name": "wrestlers tone 2",
+ "shortname": ":wrestlers_tone2:",
+ "category": "activity",
"aliases": [
- ":left_writing_hand:"
+ ":wrestling_tone2:"
],
"aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼🏼"
+ },
+ "wrestlers_tone3": {
+ "unicode": "1F93C-1F3FD",
+ "unicode_alternates": [],
+ "name": "wrestlers tone 3",
+ "shortname": ":wrestlers_tone3:",
+ "category": "activity",
+ "aliases": [
+ ":wrestling_tone3:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼🏽"
+ },
+ "wrestlers_tone4": {
+ "unicode": "1F93C-1F3FE",
+ "unicode_alternates": [],
+ "name": "wrestlers tone 4",
+ "shortname": ":wrestlers_tone4:",
+ "category": "activity",
+ "aliases": [
+ ":wrestling_tone4:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼🏾"
+ },
+ "wrestlers_tone5": {
+ "unicode": "1F93C-1F3FF",
+ "unicode_alternates": [],
+ "name": "wrestlers tone 5",
+ "shortname": ":wrestlers_tone5:",
+ "category": "activity",
+ "aliases": [
+ ":wrestling_tone5:"
+ ],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🤼🏿"
+ },
+ "writing_hand": {
+ "unicode": "270D",
+ "unicode_alternates": [
+ "270D-FE0F"
+ ],
+ "name": "writing hand",
+ "shortname": ":writing_hand:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
"keywords": [
+ "body",
+ "hands",
"write",
- "sign",
- "signature",
- "draw"
- ]
+ "diversity"
+ ],
+ "moji": "✍"
},
"writing_hand_tone1": {
"unicode": "270D-1F3FB",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "writing hand tone 1",
"shortname": ":writing_hand_tone1:",
"category": "people",
@@ -29594,11 +33225,12 @@
"sign",
"signature",
"draw"
- ]
+ ],
+ "moji": "✍🏻"
},
"writing_hand_tone2": {
"unicode": "270D-1F3FC",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "writing hand tone 2",
"shortname": ":writing_hand_tone2:",
"category": "people",
@@ -29609,11 +33241,12 @@
"sign",
"signature",
"draw"
- ]
+ ],
+ "moji": "✍🏼"
},
"writing_hand_tone3": {
"unicode": "270D-1F3FD",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "writing hand tone 3",
"shortname": ":writing_hand_tone3:",
"category": "people",
@@ -29624,11 +33257,12 @@
"sign",
"signature",
"draw"
- ]
+ ],
+ "moji": "✍🏽"
},
"writing_hand_tone4": {
"unicode": "270D-1F3FE",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "writing hand tone 4",
"shortname": ":writing_hand_tone4:",
"category": "people",
@@ -29639,11 +33273,12 @@
"sign",
"signature",
"draw"
- ]
+ ],
+ "moji": "✍🏾"
},
"writing_hand_tone5": {
"unicode": "270D-1F3FF",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "writing hand tone 5",
"shortname": ":writing_hand_tone5:",
"category": "people",
@@ -29654,20 +33289,23 @@
"sign",
"signature",
"draw"
- ]
+ ],
+ "moji": "✍🏿"
},
"x": {
"unicode": "274C",
"unicode_alternates": [],
"name": "cross mark",
"shortname": ":x:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"delete",
"no",
- "remove"
+ "remove",
+ "symbol",
+ "sol"
],
"moji": "❌"
},
@@ -29676,7 +33314,7 @@
"unicode_alternates": [],
"name": "yellow heart",
"shortname": ":yellow_heart:",
- "category": "emoticons",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29687,7 +33325,6 @@
"yellow",
"gold",
"heart",
- "love",
"friendship",
"happy",
"happiness",
@@ -29696,7 +33333,8 @@
"respectful",
"honest",
"caring",
- "selfless"
+ "selfless",
+ "symbol"
],
"moji": "💛"
},
@@ -29715,10 +33353,7 @@
"money",
"yen",
"japan",
- "japanese",
"banknote",
- "money",
- "currency",
"paper",
"cash",
"bill"
@@ -29727,7 +33362,7 @@
},
"yin_yang": {
"unicode": "262F",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "yin yang",
"shortname": ":yin_yang:",
"category": "symbols",
@@ -29739,14 +33374,15 @@
"symbol",
"tao",
"taoist"
- ]
+ ],
+ "moji": "☯"
},
"yum": {
"unicode": "1F60B",
"unicode_alternates": [],
"name": "face savouring delicious food",
"shortname": ":yum:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
@@ -29762,7 +33398,12 @@
"yummy",
"yum",
"tasty",
- "savory"
+ "savory",
+ "silly",
+ "smiley",
+ "emotion",
+ "sarcastic",
+ "good"
],
"moji": "😋"
},
@@ -29779,7 +33420,9 @@
"keywords": [
"lightning bolt",
"thunder",
- "weather"
+ "weather",
+ "sky",
+ "diarrhea"
],
"moji": "⚡"
},
@@ -29789,20 +33432,23 @@
"unicode_alternates": [
"0030-FE0F-20E3"
],
- "name": "digit zero",
+ "name": "keycap digit zero",
"shortname": ":zero:",
- "category": "other",
+ "category": "symbols",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"blue-square",
"null",
- "numbers"
+ "numbers",
+ "number",
+ "math",
+ "symbol"
]
},
"zipper_mouth": {
"unicode": "1F910",
- "unicode_alternates": "",
+ "unicode_alternates": [],
"name": "zipper-mouth face",
"shortname": ":zipper_mouth:",
"category": "people",
@@ -29810,19 +33456,24 @@
":zipper_mouth_face:"
],
"aliases_ascii": [],
- "keywords": []
+ "keywords": [
+ "mad",
+ "smiley"
+ ],
+ "moji": "🤐"
},
"zzz": {
"unicode": "1F4A4",
"unicode_alternates": [],
"name": "sleeping symbol",
"shortname": ":zzz:",
- "category": "emoticons",
+ "category": "people",
"aliases": [],
"aliases_ascii": [],
"keywords": [
"sleepy",
- "tired"
+ "tired",
+ "goodnight"
],
"moji": "💤"
}
diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb
index 27acc75dcc4..aad8626a720 100644
--- a/generator_templates/active_record/migration/create_table_migration.rb
+++ b/generator_templates/active_record/migration/create_table_migration.rb
@@ -4,6 +4,14 @@
class <%= migration_class_name %> < 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
diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb
index 06bdea11367..825bc8bdf61 100644
--- a/generator_templates/active_record/migration/migration.rb
+++ b/generator_templates/active_record/migration/migration.rb
@@ -4,6 +4,14 @@
class <%= migration_class_name %> < 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
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
new file mode 100644
index 00000000000..d02b469dac8
--- /dev/null
+++ b/lib/api/access_requests.rb
@@ -0,0 +1,90 @@
+module API
+ class AccessRequests < Grape::API
+ before { authenticate! }
+
+ helpers ::API::Helpers::MembersHelpers
+
+ %w[group project].each do |source_type|
+ resource source_type.pluralize do
+ # Get a list of group/project access requests viewable by the authenticated user.
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ #
+ # Example Request:
+ # GET /groups/:id/access_requests
+ # GET /projects/:id/access_requests
+ get ":id/access_requests" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+
+ access_requesters = paginate(source.requesters.includes(:user))
+
+ present access_requesters.map(&:user), with: Entities::AccessRequester, access_requesters: access_requesters
+ end
+
+ # Request access to the group/project
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ #
+ # Example Request:
+ # POST /groups/:id/access_requests
+ # POST /projects/:id/access_requests
+ post ":id/access_requests" do
+ source = find_source(source_type, params[:id])
+ access_requester = source.request_access(current_user)
+
+ if access_requester.persisted?
+ present access_requester.user, with: Entities::AccessRequester, access_requester: access_requester
+ else
+ render_validation_error!(access_requester)
+ end
+ end
+
+ # Approve a group/project access request
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the access requester
+ # access_level (optional) - Access level
+ #
+ # Example Request:
+ # PUT /groups/:id/access_requests/:user_id/approve
+ # PUT /projects/:id/access_requests/:user_id/approve
+ put ':id/access_requests/:user_id/approve' do
+ required_attributes! [:user_id]
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+
+ member = source.requesters.find_by!(user_id: params[:user_id])
+ if params[:access_level]
+ member.update(access_level: params[:access_level])
+ end
+ member.accept_request
+
+ status :created
+ present member.user, with: Entities::Member, member: member
+ end
+
+ # Deny a group/project access request
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the access requester
+ #
+ # Example Request:
+ # DELETE /groups/:id/access_requests/:user_id
+ # DELETE /projects/:id/access_requests/:user_id
+ delete ":id/access_requests/:user_id" do
+ required_attributes! [:user_id]
+ source = find_source(source_type, params[:id])
+
+ access_requester = source.requesters.find_by!(user_id: params[:user_id])
+
+ ::Members::DestroyService.new(access_requester, current_user).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 3d7d67510a8..4602e627fdb 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,49 +3,54 @@ module API
include APIGuard
version 'v3', using: :path
+ rescue_from Gitlab::Access::AccessDeniedError do
+ rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
+ end
+
rescue_from ActiveRecord::RecordNotFound do
rack_response({ 'message' => '404 Not found' }.to_json, 404)
end
- rescue_from :all do |exception|
- # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
- # why is this not wrapped in something reusable?
- trace = exception.backtrace
-
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << trace.join("\n ")
+ # Retain 405 error rather than a 500 error for Grape 0.15.0+.
+ # See: https://github.com/ruby-grape/grape/commit/252bfd27c320466ec3c0751812cf44245e97e5de
+ rescue_from Grape::Exceptions::Base do |e|
+ error! e.message, e.status, e.headers
+ end
- API.logger.add Logger::FATAL, message
- rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
+ rescue_from :all do |exception|
+ handle_api_exception(exception)
end
format :json
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
+ helpers ::SentryHelper
helpers ::API::Helpers
+ mount ::API::AccessRequests
mount ::API::AwardEmoji
mount ::API::Branches
mount ::API::Builds
mount ::API::CommitStatuses
mount ::API::Commits
mount ::API::DeployKeys
+ mount ::API::Deployments
+ mount ::API::Environments
mount ::API::Files
- mount ::API::GroupMembers
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
+ mount ::API::Members
mount ::API::MergeRequests
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::Pipelines
mount ::API::ProjectHooks
- mount ::API::ProjectMembers
mount ::API::ProjectSnippets
mount ::API::Projects
mount ::API::Repositories
@@ -62,5 +67,6 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
+ mount ::API::MergeRequestDiffs
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index c4fa1838b5a..2efe7e3adf3 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -56,9 +56,9 @@ module API
not_found!('Award Emoji') unless can_read_awardable?
- award = awardable.award_emoji.new(name: params[:name], user: current_user)
+ award = awardable.create_award_emoji(params[:name], current_user)
- if award.save
+ if award.persisted?
present award, with: Entities::AwardEmoji
else
not_found!("Award Emoji #{award.errors.messages}")
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index d467eb9d474..b615703df93 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -15,7 +15,8 @@ module API
# GET /projects/:id/repository/branches
get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name)
- present branches, with: Entities::RepoObject, project: user_project
+
+ present branches, with: Entities::RepoBranch, project: user_project
end
# Get a single branch
@@ -28,14 +29,21 @@ module API
get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
@branch = user_project.repository.branches.find { |item| item.name == params[:branch] }
not_found!("Branch") unless @branch
- present @branch, with: Entities::RepoObject, project: user_project
+
+ present @branch, with: Entities::RepoBranch, project: user_project
end
# Protect a single branch
#
+ # Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}`
+ # in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
+ # but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
+ #
# Parameters:
# id (required) - The ID of a project
# branch (required) - The name of the branch
+ # developers_can_push (optional) - Flag if developers can push to that branch
+ # developers_can_merge (optional) - Flag if developers can merge to that branch
# Example Request:
# PUT /projects/:id/repository/branches/:branch/protect
put ':id/repository/branches/:branch/protect',
@@ -43,11 +51,46 @@ module API
authorize_admin_project
@branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch") unless @branch
+ not_found!('Branch') unless @branch
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
- user_project.protected_branches.create(name: @branch.name) unless protected_branch
- present @branch, with: Entities::RepoObject, project: user_project
+ developers_can_merge = to_boolean(params[:developers_can_merge])
+ developers_can_push = to_boolean(params[:developers_can_push])
+
+ protected_branch_params = {
+ name: @branch.name
+ }
+
+ # If `developers_can_merge` is switched off, _all_ `DEVELOPER`
+ # merge_access_levels need to be deleted.
+ if developers_can_merge == false
+ protected_branch.merge_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
+ end
+
+ # If `developers_can_push` is switched off, _all_ `DEVELOPER`
+ # push_access_levels need to be deleted.
+ if developers_can_push == false
+ protected_branch.push_access_levels.where(access_level: Gitlab::Access::DEVELOPER).destroy_all
+ end
+
+ protected_branch_params.merge!(
+ merge_access_levels_attributes: [{
+ access_level: developers_can_merge ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }],
+ push_access_levels_attributes: [{
+ access_level: developers_can_push ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER
+ }]
+ )
+
+ if protected_branch
+ service = ProtectedBranches::UpdateService.new(user_project, current_user, protected_branch_params)
+ service.execute(protected_branch)
+ else
+ service = ProtectedBranches::CreateService.new(user_project, current_user, protected_branch_params)
+ service.execute
+ end
+
+ present @branch, with: Entities::RepoBranch, project: user_project
end
# Unprotect a single branch
@@ -66,7 +109,7 @@ module API
protected_branch = user_project.protected_branches.find_by(name: @branch.name)
protected_branch.destroy if protected_branch
- present @branch, with: Entities::RepoObject, project: user_project
+ present @branch, with: Entities::RepoBranch, project: user_project
end
# Create branch
@@ -84,7 +127,7 @@ module API
if result[:status] == :success
present result[:branch],
- with: Entities::RepoObject,
+ with: Entities::RepoBranch,
project: user_project
else
render_api_error!(result[:message], 400)
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
index d36047acd1f..52bdbcae5a8 100644
--- a/lib/api/builds.rb
+++ b/lib/api/builds.rb
@@ -52,8 +52,7 @@ module API
get ':id/builds/:build_id' do
authorize_read_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
@@ -69,18 +68,27 @@ module API
get ':id/builds/:build_id/artifacts' do
authorize_read_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
- artifacts_file = build.artifacts_file
+ present_artifacts!(build.artifacts_file)
+ end
- unless artifacts_file.file_storage?
- return redirect_to build.artifacts_file.url
- end
+ # Download the artifacts file from ref_name and job
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # ref_name (required) - The ref from repository
+ # job (required) - The name for the build
+ # Example Request:
+ # GET /projects/:id/builds/artifacts/:ref_name/download?job=name
+ get ':id/builds/artifacts/:ref_name/download',
+ requirements: { ref_name: /.+/ } do
+ authorize_read_builds!
- return not_found! unless artifacts_file.exists?
+ builds = user_project.latest_successful_builds_for(params[:ref_name])
+ latest_build = builds.find_by!(name: params[:job])
- present_file!(artifacts_file.path, artifacts_file.filename)
+ present_artifacts!(latest_build.artifacts_file)
end
# Get a trace of a specific build of a project
@@ -97,8 +105,7 @@ module API
get ':id/builds/:build_id/trace' do
authorize_read_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
content_type 'text/plain'
@@ -118,8 +125,7 @@ module API
post ':id/builds/:build_id/cancel' do
authorize_update_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
build.cancel
@@ -137,8 +143,7 @@ module API
post ':id/builds/:build_id/retry' do
authorize_update_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
return forbidden!('Build is not retryable') unless build.retryable?
build = Ci::Build.retry(build, current_user)
@@ -157,8 +162,7 @@ module API
post ':id/builds/:build_id/erase' do
authorize_update_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build
+ build = get_build!(params[:build_id])
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
@@ -176,8 +180,8 @@ module API
post ':id/builds/:build_id/artifacts/keep' do
authorize_update_builds!
- build = get_build(params[:build_id])
- return not_found!(build) unless build && build.artifacts?
+ build = get_build!(params[:build_id])
+ return not_found!(build) unless build.artifacts?
build.keep_artifacts!
@@ -185,6 +189,27 @@ module API
present build, with: Entities::Build,
user_can_download_artifacts: can?(current_user, :read_build, user_project)
end
+
+ desc 'Trigger a manual build' do
+ success Entities::Build
+ detail 'This feature was added in GitLab 8.11'
+ end
+ params do
+ requires :build_id, type: Integer, desc: 'The ID of a Build'
+ end
+ post ":id/builds/:build_id/play" do
+ authorize_read_builds!
+
+ build = get_build!(params[:build_id])
+
+ bad_request!("Unplayable Build") unless build.playable?
+
+ build.play(current_user)
+
+ status 200
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ end
end
helpers do
@@ -192,6 +217,20 @@ module API
user_project.builds.find_by(id: id.to_i)
end
+ def get_build!(id)
+ get_build(id) || not_found!
+ end
+
+ def present_artifacts!(artifacts_file)
+ if !artifacts_file.file_storage?
+ redirect_to(build.artifacts_file.url)
+ elsif artifacts_file.exists?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ not_found!
+ end
+ end
+
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 323a7086890..4df6ca8333e 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -24,7 +24,7 @@ module API
pipelines = user_project.pipelines.where(sha: params[:sha])
statuses = ::CommitStatus.where(pipeline: pipelines)
- statuses = statuses.latest unless parse_boolean(params[:all])
+ statuses = statuses.latest unless to_boolean(params[:all])
statuses = statuses.where(ref: params[:ref]) if params[:ref].present?
statuses = statuses.where(stage: params[:stage]) if params[:stage].present?
statuses = statuses.where(name: params[:name]) if params[:name].present?
@@ -64,7 +64,7 @@ module API
ref = branches.first
end
- pipeline = @project.ensure_pipeline(commit.sha, ref)
+ pipeline = @project.ensure_pipeline(commit.sha, ref, current_user)
name = params[:name] || params[:context]
status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 4a11c8e3620..b4eaf1813d4 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -54,7 +54,7 @@ module API
sha = params[:sha]
commit = user_project.commit(sha)
not_found! "Commit" unless commit
- commit.diffs.to_a
+ commit.raw_diffs.to_a
end
# Get a commit's comments
@@ -96,7 +96,7 @@ module API
}
if params[:path] && params[:line] && params[:line_type]
- commit.diffs(all_diffs: true).each do |diff|
+ commit.raw_diffs(all_diffs: true).each do |diff|
next unless diff.new_path == params[:path]
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 06eb7756841..825e05fbae3 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -2,73 +2,114 @@ module API
# Projects API
class DeployKeys < Grape::API
before { authenticate! }
- before { authorize_admin_project }
+ get "deploy_keys" do
+ authenticated_as_admin!
+
+ keys = DeployKey.all
+ present keys, with: Entities::SSHKey
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of the project'
+ end
resource :projects do
- # Get a specific project's keys
- #
- # Example Request:
- # GET /projects/:id/keys
- get ":id/keys" do
- present user_project.deploy_keys, with: Entities::SSHKey
- end
+ before { authorize_admin_project }
- # Get single key owned by currently authenticated user
+ # Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
+ # Use "projects/:id/deploy_keys/..." instead.
#
- # Example Request:
- # GET /projects/:id/keys/:id
- get ":id/keys/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
- present key, with: Entities::SSHKey
- end
+ %w(keys deploy_keys).each do |path|
+ desc "Get a specific project's deploy keys" do
+ success Entities::SSHKey
+ end
+ get ":id/#{path}" do
+ present user_project.deploy_keys, with: Entities::SSHKey
+ end
- # Add new ssh key to currently authenticated user
- # If deploy key already exists - it will be joined to project
- # but only if original one was is accessible by same user
- #
- # Parameters:
- # key (required) - New SSH Key
- # title (required) - New SSH Key's title
- # Example Request:
- # POST /projects/:id/keys
- post ":id/keys" do
- attrs = attributes_for_keys [:title, :key]
+ desc 'Get single deploy key' do
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ get ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find params[:key_id]
+ present key, with: Entities::SSHKey
+ end
- if attrs[:key].present?
- attrs[:key].strip!
+ # TODO: for 9.0 we should check if params are there with the params block
+ # grape provides, at this point we'd change behaviour so we can't
+ # Behaviour now if you don't provide all required params: it renders a
+ # validation error or two.
+ desc 'Add new deploy key to currently authenticated user' do
+ success Entities::SSHKey
+ end
+ post ":id/#{path}" do
+ attrs = attributes_for_keys [:title, :key]
+ attrs[:key].strip! if attrs[:key]
- # check if key already exist in project
key = user_project.deploy_keys.find_by(key: attrs[:key])
- if key
- present key, with: Entities::SSHKey
- return
- end
+ present key, with: Entities::SSHKey if key
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
- return
+ end
+
+ key = DeployKey.new attrs
+
+ if key.valid? && user_project.deploy_keys << key
+ present key, with: Entities::SSHKey
+ else
+ render_validation_error!(key)
end
end
- key = DeployKey.new attrs
+ desc 'Enable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ post ":id/#{path}/:key_id/enable" do
+ key = ::Projects::EnableDeployKeyService.new(user_project,
+ current_user, declared(params)).execute
- if key.valid? && user_project.deploy_keys << key
- present key, with: Entities::SSHKey
- else
- render_validation_error!(key)
+ if key
+ present key, with: Entities::SSHKey
+ else
+ not_found!('Deploy Key')
+ end
end
- end
- # Delete existed ssh key of currently authenticated user
- #
- # Example Request:
- # DELETE /projects/:id/keys/:id
- delete ":id/keys/:key_id" do
- key = user_project.deploy_keys.find params[:key_id]
- key.destroy
+ desc 'Disable a deploy key for a project' do
+ detail 'This feature was added in GitLab 8.11'
+ success Entities::SSHKey
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id/disable" do
+ key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
+ key.destroy
+
+ present key.deploy_key, with: Entities::SSHKey
+ end
+
+ desc 'Delete existing deploy key of currently authenticated user' do
+ success Key
+ end
+ params do
+ requires :key_id, type: Integer, desc: 'The ID of the deploy key'
+ end
+ delete ":id/#{path}/:key_id" do
+ key = user_project.deploy_keys.find(params[:key_id])
+ key.destroy
+ end
end
end
end
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
new file mode 100644
index 00000000000..f782bcaf7e9
--- /dev/null
+++ b/lib/api/deployments.rb
@@ -0,0 +1,40 @@
+module API
+ # Deployments RESTfull API endpoints
+ class Deployments < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all deployments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ 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'
+ end
+ get ':id/deployments' do
+ authorize! :read_deployment, user_project
+
+ present paginate(user_project.deployments), with: Entities::Deployment
+ end
+
+ desc 'Gets a specific deployment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Deployment
+ end
+ params do
+ requires :deployment_id, type: Integer, desc: 'The deployment ID'
+ end
+ get ':id/deployments/:deployment_id' do
+ authorize! :read_deployment, user_project
+
+ deployment = user_project.deployments.find(params[:deployment_id])
+
+ present deployment, with: Entities::Deployment
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9076a0c3831..cbb324dd06d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -48,12 +48,14 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
- expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :issues_events, :merge_requests_events, :tag_push_events
+ expose :note_events, :build_events, :pipeline_events, :wiki_page_events
expose :enable_ssl_verification
end
class BasicProjectDetails < Grape::Entity
expose :id
+ expose :http_url_to_repo, :web_url
expose :name, :name_with_namespace
expose :path, :path_with_namespace
end
@@ -88,11 +90,24 @@ module API
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
+ expose :only_allow_merge_if_build_succeeds
end
- class ProjectMember < UserBasic
+ class Member < UserBasic
expose :access_level do |user, options|
- options[:project].project_members.find_by(user_id: user.id).access_level
+ member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member.access_level
+ end
+ expose :expires_at do |user, options|
+ member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member.expires_at
+ end
+ end
+
+ class AccessRequester < UserBasic
+ expose :requested_at do |user, options|
+ access_requester = options[:access_requester] || options[:access_requesters].find { |m| m.user_id == user.id }
+ access_requester.requested_at
end
end
@@ -107,27 +122,27 @@ module API
expose :shared_projects, using: Entities::Project
end
- class GroupMember < UserBasic
- expose :access_level do |user, options|
- options[:group].group_members.find_by(user_id: user.id).access_level
+ class RepoBranch < Grape::Entity
+ expose :name
+
+ expose :commit do |repo_branch, options|
+ options[:project].repository.commit(repo_branch.target)
end
- end
- class RepoObject < Grape::Entity
- expose :name
+ expose :protected do |repo_branch, options|
+ options[:project].protected_branch? repo_branch.name
+ end
- expose :commit do |repo_obj, options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit
- elsif options[:project]
- options[:project].repository.commit(repo_obj.target)
- end
+ expose :developers_can_push do |repo_branch, options|
+ project = options[:project]
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
- expose :protected do |repo, options|
- if options[:project]
- options[:project].protected_branch? repo.name
- end
+ expose :developers_can_merge do |repo_branch, options|
+ project = options[:project]
+ access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
+ access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end
end
@@ -146,8 +161,13 @@ module API
expose :safe_message, as: :message
end
+ class RepoCommitStats < Grape::Entity
+ expose :additions, :deletions, :total
+ end
+
class RepoCommitDetail < RepoCommit
expose :parent_ids, :committed_date, :authored_date
+ expose :stats, using: Entities::RepoCommitStats
expose :status
end
@@ -158,6 +178,10 @@ module API
# TODO (rspeicher): Deprecated; remove in 9.0
expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ Gitlab::UrlBuilder.build(snippet)
+ end
end
class ProjectEntity < Grape::Entity
@@ -186,6 +210,11 @@ module API
end
expose :user_notes_count
expose :upvotes, :downvotes
+ expose :due_date
+
+ expose :web_url do |issue, options|
+ Gitlab::UrlBuilder.build(issue)
+ end
end
class ExternalIssue < Grape::Entity
@@ -199,7 +228,6 @@ module API
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
- expose :description
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
expose :merge_when_build_succeeds
@@ -208,11 +236,30 @@ module API
merge_request.subscribed?(options[:current_user])
end
expose :user_notes_count
+ expose :should_remove_source_branch?, as: :should_remove_source_branch
+ expose :force_remove_source_branch?, as: :force_remove_source_branch
+
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
end
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
- compare.diffs(all_diffs: true).to_a
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
+ class MergeRequestDiff < Grape::Entity
+ expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+ :created_at, :merge_request_id, :state, :real_size
+ end
+
+ class MergeRequestDiffFull < MergeRequestDiff
+ expose :commits, using: Entities::RepoCommit
+
+ expose :diffs, using: Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
end
end
@@ -313,7 +360,7 @@ module API
expose :id, :path, :kind
end
- class Member < Grape::Entity
+ class MemberAccess < Grape::Entity
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
@@ -322,15 +369,16 @@ module API
end
end
- class ProjectAccess < Member
+ class ProjectAccess < MemberAccess
end
- class GroupAccess < Member
+ class GroupAccess < MemberAccess
end
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :push_events, :issues_events, :merge_requests_events
+ expose :tag_push_events, :note_events, :build_events, :pipeline_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -409,7 +457,9 @@ module API
expose :default_project_visibility
expose :default_snippet_visibility
expose :default_group_visibility
- expose :restricted_signup_domains
+ expose :domain_whitelist
+ expose :domain_blacklist_enabled
+ expose :domain_blacklist
expose :user_oauth_applications
expose :after_sign_out_path
expose :container_registry_token_expire_delay
@@ -422,27 +472,14 @@ module API
end
class RepoTag < Grape::Entity
- expose :name
- expose :message do |repo_obj, _options|
- if repo_obj.respond_to?(:message)
- repo_obj.message
- else
- nil
- end
- end
+ expose :name, :message
- expose :commit do |repo_obj, options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit
- elsif options[:project]
- options[:project].repository.commit(repo_obj.target)
- end
+ expose :commit do |repo_tag, options|
+ options[:project].repository.commit(repo_tag.target)
end
- expose :release, using: Entities::Release do |repo_obj, options|
- if options[:project]
- options[:project].releases.find_by(tag: repo_obj.name)
- end
+ expose :release, using: Entities::Release do |repo_tag, options|
+ options[:project].releases.find_by(tag: repo_tag.name)
end
end
@@ -495,6 +532,29 @@ module API
expose :key, :value
end
+ class Pipeline < Grape::Entity
+ expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
+
+ expose :user, with: Entities::UserBasic
+ expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
+ expose :duration
+ end
+
+ class EnvironmentBasic < Grape::Entity
+ expose :id, :name, :external_url
+ end
+
+ class Environment < EnvironmentBasic
+ expose :project, using: Entities::Project
+ end
+
+ class Deployment < Grape::Entity
+ expose :id, :iid, :ref, :sha, :created_at
+ expose :user, using: Entities::UserBasic
+ expose :environment, using: Entities::EnvironmentBasic
+ expose :deployable, using: Entities::Build
+ end
+
class RepoLicense < Grape::Entity
expose :key, :name, :nickname
expose :featured, as: :popular
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
new file mode 100644
index 00000000000..819f80d8365
--- /dev/null
+++ b/lib/api/environments.rb
@@ -0,0 +1,83 @@
+module API
+ # Environments RESTfull API endpoints
+ class Environments < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all environments of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ 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'
+ end
+ get ':id/environments' do
+ authorize! :read_environment, user_project
+
+ present paginate(user_project.environments), with: Entities::Environment
+ end
+
+ desc 'Creates a new environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ 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'
+ 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)
+
+ if environment.persisted?
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
+ desc 'Updates an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ 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'
+ 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
+ if environment.update(update_params)
+ present environment, with: Entities::Environment
+ else
+ render_validation_error!(environment)
+ end
+ end
+
+ desc 'Deletes an existing environment' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Environment
+ end
+ params do
+ requires :environment_id, type: Integer, desc: 'The environment ID'
+ end
+ delete ':id/environments/:environment_id' do
+ authorize! :update_environment, user_project
+
+ environment = user_project.environments.find(params[:environment_id])
+
+ present environment.destroy, with: Entities::Environment
+ end
+ end
+ end
+end
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
deleted file mode 100644
index dbe5bb08d3f..00000000000
--- a/lib/api/group_members.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-module API
- class GroupMembers < Grape::API
- before { authenticate! }
-
- resource :groups do
- # Get a list of group members viewable by the authenticated user.
- #
- # Example Request:
- # GET /groups/:id/members
- get ":id/members" do
- group = find_group(params[:id])
- users = group.users
- present users, with: Entities::GroupMember, group: group
- end
-
- # Add a user to the list of group members
- #
- # Parameters:
- # id (required) - group id
- # user_id (required) - the users id
- # access_level (required) - Project access level
- # Example Request:
- # POST /groups/:id/members
- post ":id/members" do
- group = find_group(params[:id])
- authorize! :admin_group, group
- required_attributes! [:user_id, :access_level]
-
- unless validate_access_level?(params[:access_level])
- render_api_error!("Wrong access level", 422)
- end
-
- if group.group_members.find_by(user_id: params[:user_id])
- render_api_error!("Already exists", 409)
- end
-
- group.add_users([params[:user_id]], params[:access_level], current_user)
- member = group.group_members.find_by(user_id: params[:user_id])
- present member.user, with: Entities::GroupMember, group: group
- end
-
- # Update group member
- #
- # Parameters:
- # id (required) - The ID of a group
- # user_id (required) - The ID of a group member
- # access_level (required) - Project access level
- # Example Request:
- # PUT /groups/:id/members/:user_id
- put ':id/members/:user_id' do
- group = find_group(params[:id])
- authorize! :admin_group, group
- required_attributes! [:access_level]
-
- group_member = group.group_members.find_by(user_id: params[:user_id])
- not_found!('User can not be found') if group_member.nil?
-
- if group_member.update_attributes(access_level: params[:access_level])
- @member = group_member.user
- present @member, with: Entities::GroupMember, group: group
- else
- handle_member_errors group_member.errors
- end
- end
-
- # Remove member.
- #
- # Parameters:
- # id (required) - group id
- # user_id (required) - the users id
- #
- # Example Request:
- # DELETE /groups/:id/members/:user_id
- delete ":id/members/:user_id" do
- group = find_group(params[:id])
- authorize! :admin_group, group
- member = group.group_members.find_by(user_id: params[:user_id])
-
- if member.nil?
- render_api_error!("404 Not Found - user_id:#{params[:user_id]} not a member of group #{group.name}", 404)
- else
- member.destroy
- end
- end
- end
- end
-end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 77e407b54c5..da4b1bf9902 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -5,8 +5,11 @@ module API
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def parse_boolean(value)
- [ true, 1, '1', 't', 'T', 'true', 'TRUE', 'on', 'ON' ].include?(value)
+ def to_boolean(value)
+ return true if value =~ /^(true|t|yes|y|1|on)$/i
+ return false if value =~ /^(false|f|no|n|0|off)$/i
+
+ nil
end
def find_user_by_private_token
@@ -17,7 +20,7 @@ module API
def current_user
@current_user ||= (find_user_by_private_token || doorkeeper_guard)
- unless @current_user && Gitlab::UserAccess.allowed?(@current_user)
+ unless @current_user && Gitlab::UserAccess.new(@current_user).allowed?
return nil
end
@@ -25,7 +28,7 @@ module API
# If the sudo is the current user do nothing
if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
- render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
+ 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
@@ -46,16 +49,15 @@ module API
def user_project
@project ||= find_project(params[:id])
- @project || not_found!("Project")
end
def find_project(id)
project = Project.find_with_namespace(id) || Project.find_by(id: id)
- if project && can?(current_user, :read_project, project)
+ if can?(current_user, :read_project, project)
project
else
- nil
+ not_found!('Project')
end
end
@@ -86,11 +88,7 @@ module API
end
def find_group(id)
- begin
- group = Group.find(id)
- rescue ActiveRecord::RecordNotFound
- group = Group.find_by!(path: id)
- end
+ group = Group.find_by(path: id) || Group.find_by(id: id)
if can?(current_user, :read_group, group)
group
@@ -132,7 +130,7 @@ module API
end
def authorize!(action, subject)
- forbidden! unless abilities.allowed?(current_user, action, subject)
+ forbidden! unless can?(current_user, action, subject)
end
def authorize_push_project
@@ -194,10 +192,6 @@ module API
errors
end
- def validate_access_level?(level)
- Gitlab::Access.options_with_owner.values.include? level.to_i
- 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.
#
@@ -285,12 +279,30 @@ module API
error!({ 'message' => message }, status)
end
+ def handle_api_exception(exception)
+ if sentry_enabled? && report_exception?(exception)
+ define_params_for_grape_middleware
+ sentry_context
+ Raven.capture_exception(exception)
+ end
+
+ # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+ trace = exception.backtrace
+
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << trace.join("\n ")
+
+ API.logger.add Logger::FATAL, message
+ rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
+ end
+
# Projects helpers
def filter_projects(projects)
# If the archived parameter is passed, limit results accordingly
if params[:archived].present?
- projects = projects.where(archived: parse_boolean(params[:archived]))
+ projects = projects.where(archived: to_boolean(params[:archived]))
end
if params[:search].present?
@@ -408,11 +420,6 @@ module API
File.read(Gitlab.config.gitlab_shell.secret_file).chomp
end
- def handle_member_errors(errors)
- error!(errors[:access_level], 422) if errors[:access_level].any?
- not_found!(errors)
- end
-
def send_git_blob(repository, blob)
env['api.format'] = :txt
content_type 'text/plain'
@@ -430,5 +437,19 @@ module API
Entities::Issue
end
end
+
+ # The Grape Error Middleware only has access to env but no params. We workaround this by
+ # defining a method that returns the right value.
+ def define_params_for_grape_middleware
+ self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys }
+ end
+
+ # We could get a Grape or a standard Ruby exception. We should only report anything that
+ # is clearly an error.
+ def report_exception?(exception)
+ return true unless exception.respond_to?(:status)
+
+ exception.status == 500
+ end
end
end
diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb
new file mode 100644
index 00000000000..90114f6f667
--- /dev/null
+++ b/lib/api/helpers/members_helpers.rb
@@ -0,0 +1,13 @@
+module API
+ module Helpers
+ module MembersHelpers
+ def find_source(source_type, id)
+ public_send("find_#{source_type}", id)
+ end
+
+ def authorize_admin_source!(source_type, source)
+ authorize! :"admin_#{source_type}", source
+ end
+ end
+ end
+end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d5dfba5e0cc..5b54c11ef62 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -63,12 +63,21 @@ module API
if access_status.status
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
- response[:repository_path] = project.repository.path_to_repo
+ response[:repository_path] =
+ if wiki?
+ project.wiki.repository.path_to_repo
+ else
+ project.repository.path_to_repo
+ end
end
response
end
+ get "/merge_request_urls" do
+ ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
+ end
+
#
# Discover user by ssh key
#
@@ -92,6 +101,31 @@ module API
{}
end
end
+
+ post '/two_factor_recovery_codes' do
+ status 200
+
+ key = Key.find(params[:key_id])
+ user = key.user
+
+ # Make sure this isn't a deploy key
+ unless key.type.nil?
+ return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+ end
+
+ unless user.present?
+ return { success: false, message: 'Could not find a user for the given key' }
+ end
+
+ unless user.two_factor_enabled?
+ return { success: false, message: 'Two-factor authentication is not enabled for this user' }
+ end
+
+ codes = user.generate_otp_backup_codes!
+ user.save!
+
+ { success: true, recovery_codes: codes }
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 8a03a41e9c5..077258faee1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,8 +3,6 @@ module API
class Issues < Grape::API
before { authenticate! }
- helpers ::Gitlab::AkismetHelper
-
helpers do
def filter_issues_state(issues, state)
case state
@@ -21,17 +19,6 @@ module API
def filter_issues_milestone(issues, milestone)
issues.includes(:milestone).where('milestones.title' => milestone)
end
-
- def create_spam_log(project, current_user, attrs)
- params = attrs.merge({
- source_ip: client_ip(env),
- user_agent: user_agent(env),
- noteable_type: 'Issue',
- via_api: true
- })
-
- ::CreateSpamLogService.new(project, current_user, params).execute
- end
end
resource :issues do
@@ -152,12 +139,13 @@ module API
# 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
# Example Request:
# POST /projects/:id/issues
- post ":id/issues" do
+ post ':id/issues' do
required_attributes! [:title]
- keys = [:title, :description, :assignee_id, :milestone_id]
+ keys = [:title, :description, :assignee_id, :milestone_id, :due_date]
keys << :created_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
@@ -167,15 +155,13 @@ module API
end
project = user_project
- text = [attrs[:title], attrs[:description]].reject(&:blank?).join("\n")
- if check_for_spam?(project, current_user) && is_spam?(env, current_user, text)
- create_spam_log(project, current_user, attrs)
+ issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
+
+ if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
- issue = ::Issues::CreateService.new(project, current_user, attrs).execute
-
if issue.valid?
# Find or create labels and attach to issue. Labels are valid because
# we already checked its name, so there can't be an error here
@@ -201,12 +187,13 @@ module API
# 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
# Example Request:
# PUT /projects/:id/issues/:issue_id
- put ":id/issues/:issue_id" do
+ put ':id/issues/:issue_id' do
issue = user_project.issues.find(params[:issue_id])
authorize! :update_issue, issue
- keys = [:title, :description, :assignee_id, :milestone_id, :state_event]
+ keys = [:title, :description, :assignee_id, :milestone_id, :state_event, :due_date]
keys << :updated_at if current_user.admin? || user_project.owner == current_user
attrs = attributes_for_keys(keys)
diff --git a/lib/api/members.rb b/lib/api/members.rb
new file mode 100644
index 00000000000..94c16710d9a
--- /dev/null
+++ b/lib/api/members.rb
@@ -0,0 +1,158 @@
+module API
+ class Members < Grape::API
+ before { authenticate! }
+
+ helpers ::API::Helpers::MembersHelpers
+
+ %w[group project].each do |source_type|
+ resource source_type.pluralize do
+ # Get a list of group/project members viewable by the authenticated user.
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # query - Query string
+ #
+ # Example Request:
+ # GET /groups/:id/members
+ # GET /projects/:id/members
+ get ":id/members" do
+ source = find_source(source_type, params[:id])
+
+ members = source.members.includes(:user)
+ members = members.joins(:user).merge(User.search(params[:query])) if params[:query]
+ members = paginate(members)
+
+ present members.map(&:user), with: Entities::Member, members: members
+ end
+
+ # Get a group/project member
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the member
+ #
+ # Example Request:
+ # GET /groups/:id/members/:user_id
+ # GET /projects/:id/members/:user_id
+ get ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+
+ members = source.members
+ member = members.find_by!(user_id: params[:user_id])
+
+ present member.user, with: Entities::Member, member: member
+ end
+
+ # Add a new group/project member
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the new member
+ # access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
+ #
+ # Example Request:
+ # POST /groups/:id/members
+ # POST /projects/:id/members
+ post ":id/members" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+ required_attributes! [:user_id, :access_level]
+
+ access_requester = source.requesters.find_by(user_id: params[:user_id])
+ if access_requester
+ # We pass current_user = access_requester so that the requester doesn't
+ # receive a "access denied" email
+ ::Members::DestroyService.new(access_requester, access_requester.user).execute
+ end
+
+ member = source.members.find_by(user_id: params[:user_id])
+
+ # This is to ensure back-compatibility but 409 behavior should be used
+ # for both project and group members in 9.0!
+ conflict!('Member already exists') if source_type == 'group' && member
+
+ unless member
+ source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
+ member = source.members.find_by(user_id: params[:user_id])
+ end
+
+ if member
+ present member.user, with: Entities::Member, member: member
+ else
+ # Since `source.add_user` doesn't return a member object, we have to
+ # build a new one and populate its errors in order to render them.
+ member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
+ member.valid? # populate the errors
+
+ # This is to ensure back-compatibility but 400 behavior should be used
+ # for all validation errors in 9.0!
+ render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+ render_validation_error!(member)
+ end
+ end
+
+ # Update a group/project member
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the member
+ # access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
+ #
+ # Example Request:
+ # PUT /groups/:id/members/:user_id
+ # PUT /projects/:id/members/:user_id
+ put ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+ authorize_admin_source!(source_type, source)
+ required_attributes! [:user_id, :access_level]
+
+ member = source.members.find_by!(user_id: params[:user_id])
+ attrs = attributes_for_keys [:access_level, :expires_at]
+
+ if member.update_attributes(attrs)
+ present member.user, with: Entities::Member, member: member
+ else
+ # This is to ensure back-compatibility but 400 behavior should be used
+ # for all validation errors in 9.0!
+ render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
+ render_validation_error!(member)
+ end
+ end
+
+ # Remove a group/project member
+ #
+ # Parameters:
+ # id (required) - The group/project ID
+ # user_id (required) - The user ID of the member
+ #
+ # Example Request:
+ # DELETE /groups/:id/members/:user_id
+ # DELETE /projects/:id/members/:user_id
+ delete ":id/members/:user_id" do
+ source = find_source(source_type, params[:id])
+ required_attributes! [:user_id]
+
+ # This is to ensure back-compatibility but find_by! should be used
+ # in that casse in 9.0!
+ member = source.members.find_by(user_id: params[:user_id])
+
+ # This is to ensure back-compatibility but this should be removed in
+ # favor of find_by! in 9.0!
+ not_found!("Member: user_id:#{params[:user_id]}") if source_type == 'group' && member.nil?
+
+ # This is to ensure back-compatibility but 204 behavior should be used
+ # for all DELETE endpoints in 9.0!
+ if member.nil?
+ { message: "Access revoked", id: params[:user_id].to_i }
+ else
+ ::Members::DestroyService.new(member, current_user).execute
+
+ present member.user, with: Entities::Member, member: member
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
new file mode 100644
index 00000000000..07435d78468
--- /dev/null
+++ b/lib/api/merge_request_diffs.rb
@@ -0,0 +1,45 @@
+module API
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ desc 'Get a list of merge request diff versions' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiff
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+ end
+
+ desc 'Get a single merge request diff version' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiffFull
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 4fcdf8968c9..2b685621da9 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -242,7 +242,7 @@ module API
should_remove_source_branch: params[:should_remove_source_branch]
}
- if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.pipeline && merge_request.pipeline.active?
+ 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)
else
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
new file mode 100644
index 00000000000..2aae75c471d
--- /dev/null
+++ b/lib/api/pipelines.rb
@@ -0,0 +1,74 @@
+module API
+ class Pipelines < Grape::API
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects do
+ desc 'Get all Pipelines of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ 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'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+
+ present paginate(user_project.pipelines), with: Entities::Pipeline
+ end
+
+ desc 'Gets a specific pipeline for the project' do
+ detail 'This feature was introduced in GitLab 8.11'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id' do
+ authorize! :read_pipeline, user_project
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Retry failed builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/retry' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.retry_failed(current_user)
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Cancel all builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/cancel' do
+ authorize! :update_pipeline, user_project
+
+ pipeline.cancel_running
+
+ status 200
+ present pipeline.reload, with: Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ @pipeline ||= user_project.pipelines.find(params[:pipeline_id])
+ end
+ end
+ end
+end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 6bb70bc8bc3..14f5be3b5f6 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -45,6 +45,8 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
+ :wiki_page_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -78,6 +80,8 @@ module API
:tag_push_events,
:note_events,
:build_events,
+ :pipeline_events,
+ :wiki_page_events,
:enable_ssl_verification
]
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
deleted file mode 100644
index 6a0b3e7d134..00000000000
--- a/lib/api/project_members.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-module API
- # Projects members API
- class ProjectMembers < Grape::API
- before { authenticate! }
-
- resource :projects do
- # Get a project team members
- #
- # Parameters:
- # id (required) - The ID of a project
- # query - Query string
- # Example Request:
- # GET /projects/:id/members
- get ":id/members" do
- if params[:query].present?
- @members = paginate user_project.users.where("username LIKE ?", "%#{params[:query]}%")
- else
- @members = paginate user_project.users
- end
- present @members, with: Entities::ProjectMember, project: user_project
- end
-
- # Get a project team members
- #
- # Parameters:
- # id (required) - The ID of a project
- # user_id (required) - The ID of a user
- # Example Request:
- # GET /projects/:id/members/:user_id
- get ":id/members/:user_id" do
- @member = user_project.users.find params[:user_id]
- present @member, with: Entities::ProjectMember, project: user_project
- end
-
- # Add a new project team member
- #
- # Parameters:
- # id (required) - The ID of a project
- # user_id (required) - The ID of a user
- # access_level (required) - Project access level
- # Example Request:
- # POST /projects/:id/members
- post ":id/members" do
- authorize! :admin_project, user_project
- required_attributes! [:user_id, :access_level]
-
- # either the user is already a team member or a new one
- project_member = user_project.project_member(params[:user_id])
- if project_member.nil?
- project_member = user_project.project_members.new(
- user_id: params[:user_id],
- access_level: params[:access_level]
- )
- end
-
- if project_member.save
- @member = project_member.user
- present @member, with: Entities::ProjectMember, project: user_project
- else
- handle_member_errors project_member.errors
- end
- end
-
- # Update project team member
- #
- # Parameters:
- # id (required) - The ID of a project
- # user_id (required) - The ID of a team member
- # access_level (required) - Project access level
- # Example Request:
- # PUT /projects/:id/members/:user_id
- put ":id/members/:user_id" do
- authorize! :admin_project, user_project
- required_attributes! [:access_level]
-
- project_member = user_project.project_members.find_by(user_id: params[:user_id])
- not_found!("User can not be found") if project_member.nil?
-
- if project_member.update_attributes(access_level: params[:access_level])
- @member = project_member.user
- present @member, with: Entities::ProjectMember, project: user_project
- else
- handle_member_errors project_member.errors
- end
- end
-
- # Remove a team member from project
- #
- # Parameters:
- # id (required) - The ID of a project
- # user_id (required) - The ID of a team member
- # Example Request:
- # DELETE /projects/:id/members/:user_id
- delete ":id/members/:user_id" do
- project_member = user_project.project_members.find_by(user_id: params[:user_id])
-
- unless current_user.can?(:admin_project, user_project) ||
- current_user.can?(:destroy_project_member, project_member)
- forbidden!
- end
-
- if project_member.nil?
- { message: "Access revoked", id: params[:user_id].to_i }
- else
- project_member.destroy
- end
- end
- end
- end
-end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 0cc1edd65c8..71efd4f33ca 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -8,7 +8,7 @@ module API
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
if publik.present? && !attrs[:visibility_level].present?
- publik = parse_boolean(publik)
+ 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
@@ -25,7 +25,11 @@ module API
@projects = current_user.authorized_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ if params[:simple]
+ present @projects, with: Entities::BasicProjectDetails, user: current_user
+ else
+ present @projects, with: Entities::ProjectWithAccess, user: current_user
+ end
end
# Get an owned projects list for authenticated user
@@ -119,7 +123,8 @@ module API
:public,
:visibility_level,
:import_url,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
@@ -168,7 +173,8 @@ module API
:public,
:visibility_level,
:import_url,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
@@ -230,7 +236,8 @@ module API
:shared_runners_enabled,
:public,
:visibility_level,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
@@ -319,7 +326,7 @@ module API
# DELETE /projects/:id
delete ":id" do
authorize! :remove_project, user_project
- ::Projects::DestroyService.new(user_project, current_user, {}).pending_delete!
+ ::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
# Mark this project as forked from another
diff --git a/lib/api/session.rb b/lib/api/session.rb
index 56c202f1294..55ec66a6d67 100644
--- a/lib/api/session.rb
+++ b/lib/api/session.rb
@@ -14,6 +14,7 @@ module API
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
end
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 18408797756..b9e718147e1 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,21 +1,28 @@
module API
class Templates < Grape::API
- TEMPLATE_TYPES = {
- gitignores: Gitlab::Template::Gitignore,
- gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ GLOBAL_TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::GitignoreTemplate,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYmlTemplate
}.freeze
- TEMPLATE_TYPES.each do |template, klass|
+ helpers do
+ def render_response(template_type, template)
+ not_found!(template_type.to_s.singularize) unless template
+ present template, with: Entities::Template
+ end
+ end
+
+ GLOBAL_TEMPLATE_TYPES.each do |template_type, klass|
# Get the list of the available template
#
# Example Request:
# GET /gitignores
# GET /gitlab_ci_ymls
- get template.to_s do
+ get template_type.to_s do
present klass.all, with: Entities::TemplatesList
end
- # Get the text for a specific template
+ # Get the text for a specific template present in local filesystem
#
# Parameters:
# name (required) - The name of a template
@@ -23,13 +30,10 @@ module API
# Example Request:
# GET /gitignores/Elixir
# GET /gitlab_ci_ymls/Ruby
- get "#{template}/:name" do
+ get "#{template_type}/:name" do
required_attributes! [:name]
-
new_template = klass.find(params[:name])
- not_found!(template.to_s.singularize) unless new_template
-
- present new_template, with: Entities::Template
+ render_response(template_type, new_template)
end
end
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 2a6bfa98ca4..19df13d8aac 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -61,9 +61,9 @@ module API
#
delete ':id' do
todo = current_user.todos.find(params[:id])
- todo.done
+ TodoService.new.mark_todos_as_done([todo], current_user)
- present todo, with: Entities::Todo, current_user: current_user
+ present todo.reload, with: Entities::Todo, current_user: current_user
end
# Mark all todos as done
@@ -73,9 +73,7 @@ module API
#
delete do
todos = find_todos
- todos.each(&:done)
-
- present paginate(Kaminari.paginate_array(todos)), with: Entities::Todo, current_user: current_user
+ TodoService.new.mark_todos_as_done(todos, current_user)
end
end
end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 654b4d1c896..cedbb289f6a 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -27,7 +27,7 @@ module Backup
def backup_existing_files_dir
timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
- if File.exists?(app_files_dir)
+ if File.exist?(app_files_dir)
FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 2ff3e3bdfb0..0dfffaf0bc6 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -114,7 +114,7 @@ module Backup
tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_backup.tar")
- unless File.exists?(tar_file)
+ unless File.exist?(tar_file)
puts "The specified backup doesn't exist!"
exit 1
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index b9773f98d75..f117fc3d37d 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -28,7 +28,7 @@ module Backup
wiki = ProjectWiki.new(project)
- if File.exists?(path_to_repo(wiki))
+ if File.exist?(path_to_repo(wiki))
$progress.print " * #{wiki.path_with_namespace} ... "
if wiki.repository.empty?
$progress.puts " [SKIPPED]".color(:cyan)
@@ -49,21 +49,21 @@ module Backup
def restore
Gitlab.config.repositories.storages.each do |name, path|
- next unless File.exists?(path)
+ next unless File.exist?(path)
# Move repos dir to 'repositories.old' dir
bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
FileUtils.mv(path, bk_repos_path)
+ # This is expected from gitlab:check
+ FileUtils.mkdir_p(path, mode: 2770)
end
- FileUtils.mkdir_p(repos_path)
-
Project.find_each(batch_size: 1000) do |project|
$progress.print " * #{project.path_with_namespace} ... "
project.ensure_dir_exist
- if File.exists?(path_to_bundle(project))
+ if File.exist?(path_to_bundle(project))
FileUtils.mkdir_p(path_to_repo(project))
cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
else
@@ -80,7 +80,7 @@ module Backup
wiki = ProjectWiki.new(project)
- if File.exists?(path_to_bundle(wiki))
+ if File.exist?(path_to_bundle(wiki))
$progress.print " * #{wiki.path_with_namespace} ... "
# If a wiki bundle exists, first remove the empty repo
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 093382261ae..9ebe379f454 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,6 +3,10 @@ module Banzai
Renderer.render(text, context)
end
+ def self.cache_collection_render(texts_and_contexts)
+ Renderer.cache_collection_render(texts_and_contexts)
+ end
+
def self.render_result(text, context = {})
Renderer.render_result(text, context)
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index fac7dad3243..799b83b1069 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -31,6 +31,14 @@ module Banzai
# Text matching LINK_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
+ # The XPath query to use for finding text nodes to parse.
+ TEXT_QUERY = %Q(descendant-or-self::text()[
+ not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
+ and contains(., '://')
+ and not(starts-with(., 'http'))
+ and not(starts-with(., 'ftp'))
+ ])
+
def call
return doc if context[:autolink] == false
@@ -56,6 +64,8 @@ module Banzai
# period (e.g., http://localhost:3000/)
rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1)
+ return if rinku == html
+
# Rinku returns a String, so parse it back to a Nokogiri::XML::Document
# for further processing.
@doc = parse_html(rinku)
@@ -64,16 +74,11 @@ module Banzai
# Autolinks any text matching LINK_PATTERN that Rinku didn't already
# replace
def text_parse
- search_text_nodes(doc).each do |node|
+ doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
- next if has_ancestor?(node, IGNORE_PARENTS)
next unless content.match(LINK_PATTERN)
- # If Rinku didn't link this, there's probably a good reason, so we'll
- # skip it too
- next if content.start_with?(*%w(http https ftp))
-
html = autolink_filter(content)
next if html == content
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index ae7d31cf191..2492b5213ac 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -38,6 +38,11 @@ module Banzai
end
end
+ # Build a regexp that matches all valid :emoji: names.
+ def self.emoji_pattern
+ @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ end
+
private
def emoji_url(name)
@@ -59,11 +64,6 @@ module Banzai
ActionController::Base.helpers.url_to_image(image)
end
- # Build a regexp that matches all valid :emoji: names.
- def self.emoji_pattern
- @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
- end
-
def emoji_pattern
self.class.emoji_pattern
end
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index 9b209533a89..ff580ec68f8 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -12,7 +12,12 @@ module Banzai
html
end
- private
+ def self.renderer
+ @renderer ||= begin
+ renderer = Redcarpet::Render::HTML.new
+ Redcarpet::Markdown.new(renderer, redcarpet_options)
+ end
+ end
def self.redcarpet_options
# https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
@@ -28,12 +33,7 @@ module Banzai
}.freeze
end
- def self.renderer
- @renderer ||= begin
- renderer = Redcarpet::Render::HTML.new
- Redcarpet::Markdown.new(renderer, redcarpet_options)
- end
- end
+ private_class_method :redcarpet_options
end
end
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index c78da404607..4fa8d05481f 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -20,7 +20,7 @@ module Banzai
process_link_attr el.attribute('href')
end
- doc.search('img').each do |el|
+ doc.css('img, video').each do |el|
process_link_attr el.attribute('src')
end
@@ -35,6 +35,7 @@ module Banzai
def process_link_attr(html_attr)
return if html_attr.blank?
+ return if html_attr.value.start_with?('//')
uri = URI(html_attr.value)
if uri.relative? && uri.path.present?
@@ -51,7 +52,7 @@ module Banzai
relative_url_root,
context[:project].path_with_namespace,
uri_type(file_path),
- ref || context[:project].default_branch, # if no ref exists, point to the default branch
+ ref,
file_path
].compact.join('/').squeeze('/').chomp('/')
@@ -87,10 +88,13 @@ module Banzai
def build_relative_path(path, request_path)
return request_path if path.empty?
return path unless request_path
+ return path[1..-1] if path.start_with?('/')
parts = request_path.split('/')
parts.pop if uri_type(request_path) != :tree
+ path.sub!(%r{\A\./}, '')
+
while path.start_with?('../')
parts.pop
path.sub!('../', '')
@@ -112,8 +116,7 @@ module Banzai
end
def current_commit
- @current_commit ||= context[:commit] ||
- ref ? repository.commit(ref) : repository.head_commit
+ @current_commit ||= context[:commit] || repository.commit(ref)
end
def relative_url_root
@@ -121,7 +124,7 @@ module Banzai
end
def ref
- context[:ref]
+ context[:ref] || context[:project].default_branch
end
def repository
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index ca80aac5a08..6e13282d5f4 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -7,7 +7,7 @@ module Banzai
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
def whitelist
- whitelist = super
+ whitelist = super.dup
customize_whitelist(whitelist)
@@ -42,6 +42,8 @@ module Banzai
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
+ whitelist[:transformers] = whitelist[:transformers].dup
+
# ...but then remove links with unsafe protocols
whitelist[:transformers].push(remove_unsafe_links)
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 536b478979f..fcdb496aed2 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -17,31 +17,48 @@ module Banzai
def highlight_node(node)
language = node.attr('class')
- code = node.text
+ code = node.text
+ css_classes = "code highlight"
+ lexer = lexer_for(language)
begin
- highlighted = block_code(code, language)
+ code = format(lex(lexer, code))
+
+ css_classes << " js-syntax-highlight #{lexer.tag}"
rescue
# Gracefully handle syntax highlighter bugs/errors to ensure
# users can still access an issue/comment/etc.
- highlighted = "<pre>#{code}</pre>"
end
+ highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>)
+
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
end
private
+ # Separate method so it can be instrumented.
+ def lex(lexer, code)
+ lexer.lex(code)
+ end
+
+ def format(tokens)
+ rouge_formatter.format(tokens)
+ end
+
+ def lexer_for(language)
+ (Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
+ end
+
def replace_parent_pre_element(node, highlighted)
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
# Override Rouge::Plugins::Redcarpet#rouge_formatter
- def rouge_formatter(lexer)
- Rouge::Formatters::HTMLGitlab.new(
- cssclass: "code highlight js-syntax-highlight #{lexer.tag}")
+ def rouge_formatter(lexer = nil)
+ @rouge_formatter ||= Rouge::Formatters::HTML.new
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 5b0a6d8541b..e1ca7f4d24b 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -112,7 +112,7 @@ module Banzai
data = data_attribute(project: project.id, author: author.try(:id))
text = link_text || User.reference_prefix + 'all'
- link_tag(url, data, text)
+ link_tag(url, data, text, 'All Project and Group Members')
end
def link_to_namespace(namespace, link_text: nil)
@@ -128,7 +128,7 @@ module Banzai
data = data_attribute(group: namespace.id)
text = link_text || Group.reference_prefix + group
- link_tag(url, data, text)
+ link_tag(url, data, text, namespace.name)
end
def link_to_user(user, namespace, link_text: nil)
@@ -136,11 +136,11 @@ module Banzai
data = data_attribute(user: namespace.owner_id)
text = link_text || User.reference_prefix + user
- link_tag(url, data, text)
+ link_tag(url, data, text, namespace.owner_name)
end
- def link_tag(url, data, text)
- %(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>)
+ def link_tag(url, data, text, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>)
end
end
end
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
new file mode 100644
index 00000000000..ac7bbcb0d10
--- /dev/null
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -0,0 +1,56 @@
+module Banzai
+ module Filter
+ # Find every image that isn't already wrapped in an `a` tag, and that has
+ # a `src` attribute ending with a video extension, add a new video node and
+ # a "Download" link in the case the video cannot be played.
+ class VideoLinkFilter < HTML::Pipeline::Filter
+ def call
+ doc.xpath(query).each do |el|
+ el.replace(video_node(doc, el))
+ end
+
+ doc
+ end
+
+ private
+
+ def query
+ @query ||= begin
+ src_query = UploaderHelper::VIDEO_EXT.map do |ext|
+ "'.#{ext}' = substring(@src, string-length(@src) - #{ext.size})"
+ end
+
+ "descendant-or-self::img[not(ancestor::a) and (#{src_query.join(' or ')})]"
+ end
+ end
+
+ def video_node(doc, element)
+ container = doc.document.create_element(
+ 'div',
+ class: 'video-container'
+ )
+
+ video = doc.document.create_element(
+ 'video',
+ src: element['src'],
+ width: '400',
+ controls: true,
+ 'data-setup' => '{}')
+
+ link = doc.document.create_element(
+ 'a',
+ element['title'] || element['alt'],
+ href: element['src'],
+ target: '_blank',
+ title: "Download '#{element['title'] || element['alt']}'")
+ download_paragraph = doc.document.create_element('p')
+ download_paragraph.children = link
+
+ container.add_child(video)
+ container.add_child(download_paragraph)
+
+ container
+ end
+ end
+ end
+end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index f0e4f28bf12..9aef807c152 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -31,17 +31,15 @@ module Banzai
redacted = redact_documents(documents)
objects.each_with_index do |object, index|
- object.__send__("#{attribute}_html=", redacted.fetch(index))
+ redacted_data = redacted[index]
+ object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe)
+ object.user_visible_reference_count = redacted_data[:visible_reference_count]
end
-
- objects
end
# Renders the attribute of every given object.
def render_objects(objects, attribute)
- objects.map do |object|
- render_attribute(object, attribute)
- end
+ render_attributes(objects, attribute)
end
# Redacts the list of documents.
@@ -50,9 +48,7 @@ module Banzai
def redact_documents(documents)
redactor = Redactor.new(project, user)
- redactor.redact(documents).map do |document|
- document.to_html.html_safe
- end
+ redactor.redact(documents)
end
# Returns a Banzai context for the given object and attribute.
@@ -66,16 +62,21 @@ module Banzai
context
end
- # Renders the attribute of an object.
+ # Renders the attributes of a set of objects.
#
- # Returns a `Nokogiri::HTML::Document`.
- def render_attribute(object, attribute)
- context = context_for(object, attribute)
+ # Returns an Array of `Nokogiri::HTML::Document`.
+ def render_attributes(objects, attribute)
+ strings_and_contexts = objects.map do |object|
+ context = context_for(object, attribute)
- string = object.__send__(attribute)
- html = Banzai.render(string, context)
+ string = object.__send__(attribute)
- Banzai::Pipeline[:relative_link].to_document(html, context)
+ { text: string, context: context }
+ end
+
+ Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index|
+ Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context])
+ end
end
def base_context
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index b27ecf3c923..8d94b199c66 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -7,6 +7,7 @@ module Banzai
Filter::SanitizationFilter,
Filter::UploadLinkFilter,
+ Filter::VideoLinkFilter,
Filter::ImageLinkFilter,
Filter::EmojiFilter,
Filter::TableOfContentsFilter,
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index ffd267d5e9a..0df3a72d1c4 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -19,29 +19,36 @@ module Banzai
#
# Returns the documents passed as the first argument.
def redact(documents)
- nodes = documents.flat_map do |document|
- Querying.css(document, 'a.gfm[data-reference-type]')
- end
-
- redact_nodes(nodes)
+ all_document_nodes = document_nodes(documents)
- documents
+ redact_document_nodes(all_document_nodes)
end
- # Redacts the given nodes
+ # Redacts the given node documents
#
- # nodes - An Array of HTML nodes to redact.
- def redact_nodes(nodes)
- visible = nodes_visible_to_user(nodes)
+ # data - An Array of a Hashes mapping an HTML document to nodes to redact.
+ def redact_document_nodes(all_document_nodes)
+ all_nodes = all_document_nodes.map { |x| x[:nodes] }.flatten
+ visible = nodes_visible_to_user(all_nodes)
+ metadata = []
- nodes.each do |node|
- unless visible.include?(node)
+ all_document_nodes.each do |entry|
+ nodes_for_document = entry[:nodes]
+ doc_data = { document: entry[:document], visible_reference_count: nodes_for_document.count }
+ metadata << doc_data
+
+ nodes_for_document.each do |node|
+ next if visible.include?(node)
+
+ doc_data[:visible_reference_count] -= 1
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
text = node.attr('data-original') || node.text
node.replace(text)
end
end
+
+ metadata
end
# Returns the nodes visible to the current user.
@@ -65,5 +72,11 @@ module Banzai
visible
end
+
+ def document_nodes(documents)
+ documents.map do |document|
+ { document: document, nodes: Querying.css(document, 'a.gfm[data-reference-type]') }
+ end
+ end
end
end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index bf366962aef..b26a41a1f3b 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -2,11 +2,11 @@ module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
def initialize
- @texts = []
+ @texts_and_contexts = []
end
def analyze(text, context = {})
- @texts << Renderer.render(text, context)
+ @texts_and_contexts << { text: text, context: context }
end
def references(type, project, current_user = nil)
@@ -21,9 +21,10 @@ module Banzai
def html_documents
# This ensures that we don't memoize anything until we have a number of
# text blobs to parse.
- return [] if @texts.empty?
+ return [] if @texts_and_contexts.empty?
- @html_documents ||= @texts.map { |html| Nokogiri::HTML.fragment(html) }
+ @html_documents ||= Renderer.cache_collection_render(@texts_and_contexts)
+ .map { |html| Nokogiri::HTML.fragment(html) }
end
end
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index f306079d833..6c20dec5734 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -9,10 +9,11 @@ module Banzai
issues = issues_for_nodes(nodes)
- nodes.select do |node|
- issue = issue_for_node(issues, node)
+ readable_issues = Ability.
+ issues_readable_by_user(issues.values, user).to_set
- issue ? can?(user, :read_issue, issue) : false
+ nodes.select do |node|
+ readable_issues.include?(issue_for_node(issues, node))
end
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 6718acdef7e..a4ae27eefd8 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,5 +1,7 @@
module Banzai
module Renderer
+ extend self
+
# Convert a Markdown String into an HTML-safe String of HTML
#
# Note that while the returned HTML will have been sanitized of dangerous
@@ -10,11 +12,11 @@ module Banzai
# requiring XHTML, such as Atom feeds, need to call `post_process` on the
# result, providing the appropriate `pipeline` option.
#
- # markdown - Markdown String
+ # text - Markdown String
# context - Hash of context options passed to our HTML Pipeline
#
# Returns an HTML-safe String
- def self.render(text, context = {})
+ def render(text, context = {})
cache_key = context.delete(:cache_key)
cache_key = full_cache_key(cache_key, context[:pipeline])
@@ -29,7 +31,59 @@ module Banzai
end
end
- def self.render_result(text, context = {})
+ # Perform multiple render from an Array of Markdown String into an
+ # Array of HTML-safe String of HTML.
+ #
+ # As the rendered Markdown String can be already cached read all the data
+ # from the cache using Rails.cache.read_multi operation. If the Markdown String
+ # is not in the cache or it's not cacheable (no cache_key entry is provided in
+ # the context) the Markdown String is rendered and stored in the cache so the
+ # next render call gets the rendered HTML-safe String from the cache.
+ #
+ # For further explanation see #render method comments.
+ #
+ # texts_and_contexts - An Array of Hashes that contains the Markdown String (:text)
+ # an options passed to our HTML Pipeline (:context)
+ #
+ # If on the :context you specify a :cache_key entry will be used to retrieve it
+ # and cache the result of rendering the Markdown String.
+ #
+ # Returns an Array containing HTML-safe String instances.
+ #
+ # Example:
+ # texts_and_contexts
+ # => [{ text: '### Hello',
+ # context: { cache_key: [note, :note] } }]
+ def cache_collection_render(texts_and_contexts)
+ items_collection = texts_and_contexts.each_with_index do |item, index|
+ context = item[:context]
+ cache_key = full_cache_multi_key(context.delete(:cache_key), context[:pipeline])
+
+ item[:cache_key] = cache_key if cache_key
+ end
+
+ cacheable_items, non_cacheable_items = items_collection.partition { |item| item.key?(:cache_key) }
+
+ items_in_cache = []
+ items_not_in_cache = []
+
+ unless cacheable_items.empty?
+ items_in_cache = Rails.cache.read_multi(*cacheable_items.map { |item| item[:cache_key] })
+ items_not_in_cache = cacheable_items.reject do |item|
+ item[:rendered] = items_in_cache[item[:cache_key]]
+ items_in_cache.key?(item[:cache_key])
+ end
+ end
+
+ (items_not_in_cache + non_cacheable_items).each do |item|
+ item[:rendered] = render(item[:text], item[:context])
+ Rails.cache.write(item[:cache_key], item[:rendered]) if item[:cache_key]
+ end
+
+ items_collection.map { |item| item[:rendered] }
+ end
+
+ def render_result(text, context = {})
text = Pipeline[:pre_process].to_html(text, context) if text
Pipeline[context[:pipeline]].call(text, context)
@@ -48,7 +102,7 @@ module Banzai
# :user - User object
#
# Returns an HTML-safe String
- def self.post_process(html, context)
+ def post_process(html, context)
context = Pipeline[context[:pipeline]].transform_context(context)
pipeline = Pipeline[:post_process]
@@ -61,7 +115,7 @@ module Banzai
private
- def self.cacheless_render(text, context = {})
+ def cacheless_render(text, context = {})
Gitlab::Metrics.measure(:banzai_cacheless_render) do
result = render_result(text, context)
@@ -74,9 +128,17 @@ module Banzai
end
end
- def self.full_cache_key(cache_key, pipeline_name)
+ def full_cache_key(cache_key, pipeline_name)
return unless cache_key
["banzai", *cache_key, pipeline_name || :full]
end
+
+ # To map Rails.cache.read_multi results we need to know the Rails.cache.expanded_key.
+ # Other option will be to generate stringified keys on our side and don't delegate to Rails.cache.expanded_key
+ # method.
+ def full_cache_multi_key(cache_key, pipeline_name)
+ return unless cache_key
+ Rails.cache.send(:expanded_key, full_cache_key(cache_key, pipeline_name))
+ end
end
end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 17bb99a2ae5..a6b9beecded 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -9,22 +9,14 @@ module Ci
end
rescue_from :all do |exception|
- # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
- # why is this not wrapped in something reusable?
- trace = exception.backtrace
-
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << trace.join("\n ")
-
- API.logger.add Logger::FATAL, message
- rack_response({ 'message' => '500 Internal Server Error' }, 500)
+ handle_api_exception(exception)
end
content_type :txt, 'text/plain'
content_type :json, 'application/json'
format :json
+ helpers ::SentryHelper
helpers ::Ci::API::Helpers
helpers ::API::Helpers
helpers Gitlab::CurrentSettings
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 260ac81f5fa..9f3b582a263 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -20,8 +20,13 @@ module Ci
build = Ci::RegisterBuildService.new.execute(current_runner)
if build
+ Gitlab::Metrics.add_event(:build_found,
+ project: build.project.path_with_namespace)
+
present build, with: Entities::BuildDetails
else
+ Gitlab::Metrics.add_event(:build_not_found)
+
not_found!
end
end
@@ -42,6 +47,9 @@ module Ci
build.update_attributes(trace: params[:trace]) if params[:trace]
+ Gitlab::Metrics.add_event(:update_build,
+ project: build.project.path_with_namespace)
+
case params[:state].to_s
when 'success'
build.success
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
index 1d7126a432d..3decc3b1a26 100644
--- a/lib/ci/charts.rb
+++ b/lib/ci/charts.rb
@@ -1,5 +1,37 @@
module Ci
module Charts
+ module DailyInterval
+ def grouped_count(query)
+ query.
+ group("DATE(#{Ci::Build.table_name}.created_at)").
+ count(:created_at).
+ transform_keys { |date| date.strftime(@format) }
+ end
+
+ def interval_step
+ @interval_step ||= 1.day
+ end
+ end
+
+ module MonthlyInterval
+ def grouped_count(query)
+ if Gitlab::Database.postgresql?
+ query.
+ group("to_char(#{Ci::Build.table_name}.created_at, '01 Month YYYY')").
+ count(:created_at).
+ transform_keys(&:squish)
+ else
+ query.
+ group("DATE_FORMAT(#{Ci::Build.table_name}.created_at, '01 %M %Y')").
+ count(:created_at)
+ end
+ end
+
+ def interval_step
+ @interval_step ||= 1.month
+ end
+ end
+
class Chart
attr_reader :labels, :total, :success, :project, :build_times
@@ -13,47 +45,59 @@ module Ci
collect
end
- def push(from, to, format)
- @labels << from.strftime(format)
- @total << project.builds.
- where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
- count(:all)
- @success << project.builds.
- where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
- success.count(:all)
+ def collect
+ query = project.builds.
+ where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", @to, @from)
+
+ totals_count = grouped_count(query)
+ success_count = grouped_count(query.success)
+
+ current = @from
+ while current < @to
+ label = current.strftime(@format)
+
+ @labels << label
+ @total << (totals_count[label] || 0)
+ @success << (success_count[label] || 0)
+
+ current += interval_step
+ end
end
end
class YearChart < Chart
- def collect
- 13.times do |i|
- start_month = (Date.today.years_ago(1) + i.month).beginning_of_month
- end_month = start_month.end_of_month
+ include MonthlyInterval
- push(start_month, end_month, "%d %B %Y")
- end
+ def initialize(*)
+ @to = Date.today.end_of_month
+ @from = @to.years_ago(1).beginning_of_month
+ @format = '%d %B %Y'
+
+ super
end
end
class MonthChart < Chart
- def collect
- 30.times do |i|
- start_day = Date.today - 30.days + i.days
- end_day = Date.today - 30.days + i.day + 1.day
+ include DailyInterval
- push(start_day, end_day, "%d %B")
- end
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 30.days
+ @format = '%d %B'
+
+ super
end
end
class WeekChart < Chart
- def collect
- 7.times do |i|
- start_day = Date.today - 7.days + i.days
- end_day = Date.today - 7.days + i.day + 1.day
+ include DailyInterval
- push(start_day, end_day, "%d %B")
- end
+ def initialize(*)
+ @to = Date.today
+ @from = @to - 7.days
+ @format = '%d %B'
+
+ super
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 01ef13df57a..47efd5bd9f2 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,21 +4,11 @@ module Ci
include Gitlab::Ci::Config::Node::LegacyValidationHelpers
- DEFAULT_STAGE = 'test'
- ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
- :allow_failure, :type, :stage, :when, :artifacts, :cache,
- :dependencies, :before_script, :after_script, :variables,
- :environment]
- ALLOWED_CACHE_KEYS = [:key, :untracked, :paths]
- ALLOWED_ARTIFACTS_KEYS = [:name, :untracked, :paths, :when, :expire_in]
-
attr_reader :path, :cache, :stages
def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash
-
@path = path
unless @ci_config.valid?
@@ -26,64 +16,42 @@ module Ci
end
initial_parsing
- validate!
rescue Gitlab::Ci::Config::Loader::FormatError => e
raise ValidationError, e.message
end
- def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
- builds.select do |build|
- build[:stage] == stage &&
- process?(build[:only], build[:except], ref, tag, trigger_request)
+ def jobs_for_ref(ref, tag = false, trigger_request = nil)
+ @jobs.select do |_, job|
+ process?(job[:only], job[:except], ref, tag, trigger_request)
end
end
- def builds
- @jobs.map do |name, job|
- build_job(name, job)
+ def jobs_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
+ jobs_for_ref(ref, tag, trigger_request).select do |_, job|
+ job[:stage] == stage
end
end
- def global_variables
- @variables
- end
-
- def job_variables(name)
- job = @jobs[name.to_sym]
- return [] unless job
-
- job[:variables] || []
+ def builds_for_ref(ref, tag = false, trigger_request = nil)
+ jobs_for_ref(ref, tag, trigger_request).map do |name, _|
+ build_attributes(name)
+ end
end
- private
-
- def initial_parsing
- @before_script = @ci_config.before_script
- @image = @ci_config.image
- @after_script = @ci_config.after_script
- @services = @ci_config.services
- @variables = @ci_config.variables
- @stages = @ci_config.stages
- @cache = @ci_config.cache
-
- @jobs = {}
-
- @config.except!(*ALLOWED_YAML_KEYS)
- @config.each { |name, param| add_job(name, param) }
-
- raise ValidationError, "Please define at least one job" if @jobs.none?
+ def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
+ jobs_for_stage_and_ref(stage, ref, tag, trigger_request).map do |name, _|
+ build_attributes(name)
+ end
end
- def add_job(name, job)
- return if name.to_s.start_with?('.')
-
- raise ValidationError, "Unknown parameter: #{name}" unless job.is_a?(Hash) && job.has_key?(:script)
-
- stage = job[:stage] || job[:type] || DEFAULT_STAGE
- @jobs[name] = { stage: stage }.merge(job)
+ def builds
+ @jobs.map do |name, _|
+ build_attributes(name)
+ end
end
- def build_job(name, job)
+ def build_attributes(name)
+ job = @jobs[name.to_sym] || {}
{
stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
@@ -94,12 +62,11 @@ module Ci
# - before script should be a concatenated command
commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
tag_list: job[:tags] || [],
- name: name,
- only: job[:only],
- except: job[:except],
+ name: job[:name].to_s,
allow_failure: job[:allow_failure] || false,
when: job[:when] || 'on_success',
environment: job[:environment],
+ yaml_variables: yaml_variables(name),
options: {
image: job[:image] || @image,
services: job[:services] || @services,
@@ -111,154 +78,59 @@ module Ci
}
end
- def validate!
- @jobs.each do |name, job|
- validate_job!(name, job)
- end
+ private
- true
- end
+ def initial_parsing
+ ##
+ # Global config
+ #
+ @before_script = @ci_config.before_script
+ @image = @ci_config.image
+ @after_script = @ci_config.after_script
+ @services = @ci_config.services
+ @variables = @ci_config.variables
+ @stages = @ci_config.stages
+ @cache = @ci_config.cache
- def validate_job!(name, job)
- validate_job_name!(name)
- validate_job_keys!(name, job)
- validate_job_types!(name, job)
- validate_job_script!(name, job)
-
- validate_job_stage!(name, job) if job[:stage]
- validate_job_variables!(name, job) if job[:variables]
- validate_job_cache!(name, job) if job[:cache]
- validate_job_artifacts!(name, job) if job[:artifacts]
- validate_job_dependencies!(name, job) if job[:dependencies]
- end
+ ##
+ # Jobs
+ #
+ @jobs = @ci_config.jobs
- def validate_job_name!(name)
- if name.blank? || !validate_string(name)
- raise ValidationError, "job name should be non-empty string"
- end
- end
+ @jobs.each do |name, job|
+ # logical validation for job
- def validate_job_keys!(name, job)
- job.keys.each do |key|
- unless ALLOWED_JOB_KEYS.include? key
- raise ValidationError, "#{name} job: unknown parameter #{key}"
- end
+ validate_job_stage!(name, job)
+ validate_job_dependencies!(name, job)
end
end
- def validate_job_types!(name, job)
- if job[:image] && !validate_string(job[:image])
- raise ValidationError, "#{name} job: image should be a string"
- end
+ def yaml_variables(name)
+ variables = (@variables || {})
+ .merge(job_variables(name))
- if job[:services] && !validate_array_of_strings(job[:services])
- raise ValidationError, "#{name} job: services should be an array of strings"
- end
-
- if job[:tags] && !validate_array_of_strings(job[:tags])
- raise ValidationError, "#{name} job: tags parameter should be an array of strings"
- end
-
- if job[:only] && !validate_array_of_strings_or_regexps(job[:only])
- raise ValidationError, "#{name} job: only parameter should be an array of strings or regexps"
- end
-
- if job[:except] && !validate_array_of_strings_or_regexps(job[:except])
- raise ValidationError, "#{name} job: except parameter should be an array of strings or regexps"
- end
-
- if job[:allow_failure] && !validate_boolean(job[:allow_failure])
- raise ValidationError, "#{name} job: allow_failure parameter should be an boolean"
- end
-
- if job[:when] && !job[:when].in?(%w[on_success on_failure always])
- raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
- end
-
- if job[:environment] && !validate_environment(job[:environment])
- raise ValidationError, "#{name} job: environment parameter #{Gitlab::Regex.environment_name_regex_message}"
+ variables.map do |key, value|
+ { key: key, value: value, public: true }
end
end
- def validate_job_script!(name, job)
- if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name} job: script should be a string or an array of a strings"
- end
-
- if job[:before_script] && !validate_array_of_strings(job[:before_script])
- raise ValidationError, "#{name} job: before_script should be an array of strings"
- end
+ def job_variables(name)
+ job = @jobs[name.to_sym]
+ return {} unless job
- if job[:after_script] && !validate_array_of_strings(job[:after_script])
- raise ValidationError, "#{name} job: after_script should be an array of strings"
- end
+ job[:variables] || {}
end
def validate_job_stage!(name, job)
+ return unless job[:stage]
+
unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
end
end
- def validate_job_variables!(name, job)
- unless validate_variables(job[:variables])
- raise ValidationError,
- "#{name} job: variables should be a map of key-value strings"
- end
- end
-
- def validate_job_cache!(name, job)
- job[:cache].keys.each do |key|
- unless ALLOWED_CACHE_KEYS.include? key
- raise ValidationError, "#{name} job: cache unknown parameter #{key}"
- end
- end
-
- if job[:cache][:key] && !validate_string(job[:cache][:key])
- raise ValidationError, "#{name} job: cache:key parameter should be a string"
- end
-
- if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked])
- raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean"
- end
-
- if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths])
- raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings"
- end
- end
-
- def validate_job_artifacts!(name, job)
- job[:artifacts].keys.each do |key|
- unless ALLOWED_ARTIFACTS_KEYS.include? key
- raise ValidationError, "#{name} job: artifacts unknown parameter #{key}"
- end
- end
-
- if job[:artifacts][:name] && !validate_string(job[:artifacts][:name])
- raise ValidationError, "#{name} job: artifacts:name parameter should be a string"
- end
-
- if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
- raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
- end
-
- if job[:artifacts][:paths] && !validate_array_of_strings(job[:artifacts][:paths])
- raise ValidationError, "#{name} job: artifacts:paths parameter should be an array of strings"
- end
-
- if job[:artifacts][:when] && !job[:artifacts][:when].in?(%w[on_success on_failure always])
- raise ValidationError, "#{name} job: artifacts:when parameter should be on_success, on_failure or always"
- end
-
- if job[:artifacts][:expire_in] && !validate_duration(job[:artifacts][:expire_in])
- raise ValidationError, "#{name} job: artifacts:expire_in parameter should be a duration"
- end
- end
-
def validate_job_dependencies!(name, job)
- unless validate_array_of_strings(job[:dependencies])
- raise ValidationError, "#{name} job: dependencies parameter should be an array of strings"
- end
+ return unless job[:dependencies]
stage_index = @stages.index(job[:stage])
diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb
deleted file mode 100644
index bb2bdbed495..00000000000
--- a/lib/ci/static_model.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database.
-module Ci
- module StaticModel
- extend ActiveSupport::Concern
-
- module ClassMethods
- # Used by ActiveRecord's polymorphic association to set object_id
- def primary_key
- 'id'
- end
-
- # Used by ActiveRecord's polymorphic association to set object_type
- def base_class
- self
- end
- end
-
- # Used by AR for fetching attributes
- #
- # Pass it along if we respond to it.
- def [](key)
- send(key) if respond_to?(key)
- end
-
- def to_param
- id
- end
-
- def new_record?
- false
- end
-
- def persisted?
- false
- end
-
- def destroyed?
- false
- end
-
- def ==(other)
- if other.is_a? ::Ci::StaticModel
- id == other.id
- else
- super
- end
- end
- end
-end
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index 42232b7129d..2edddb84fc3 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -7,62 +7,91 @@ module ContainerRegistry
MANIFEST_VERSION = 'application/vnd.docker.distribution.manifest.v2+json'
+ # Taken from: FaradayMiddleware::FollowRedirects
+ REDIRECT_CODES = Set.new [301, 302, 303, 307]
+
def initialize(base_uri, options = {})
@base_uri = base_uri
- @faraday = Faraday.new(@base_uri) do |conn|
- initialize_connection(conn, options)
- end
+ @options = options
end
def repository_tags(name)
- response_body @faraday.get("/v2/#{name}/tags/list")
+ response_body faraday.get("/v2/#{name}/tags/list")
end
def repository_manifest(name, reference)
- response_body @faraday.get("/v2/#{name}/manifests/#{reference}")
+ response_body faraday.get("/v2/#{name}/manifests/#{reference}")
end
def repository_tag_digest(name, reference)
- response = @faraday.head("/v2/#{name}/manifests/#{reference}")
+ response = faraday.head("/v2/#{name}/manifests/#{reference}")
response.headers['docker-content-digest'] if response.success?
end
def delete_repository_tag(name, reference)
- @faraday.delete("/v2/#{name}/manifests/#{reference}").success?
+ faraday.delete("/v2/#{name}/manifests/#{reference}").success?
end
def blob(name, digest, type = nil)
- headers = {}
- headers['Accept'] = type if type
- response_body @faraday.get("/v2/#{name}/blobs/#{digest}", nil, headers)
+ type ||= 'application/octet-stream'
+ response_body faraday_blob.get("/v2/#{name}/blobs/#{digest}", nil, 'Accept' => type), allow_redirect: true
end
def delete_blob(name, digest)
- @faraday.delete("/v2/#{name}/blobs/#{digest}").success?
+ faraday.delete("/v2/#{name}/blobs/#{digest}").success?
end
-
+
private
-
+
def initialize_connection(conn, options)
conn.request :json
+
+ if options[:user] && options[:password]
+ conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
+ elsif options[:token]
+ conn.request(:authorization, :bearer, options[:token].to_s)
+ end
+
+ conn.adapter :net_http
+ end
+
+ def accept_manifest(conn)
conn.headers['Accept'] = MANIFEST_VERSION
conn.response :json, content_type: 'application/json'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+prettyjws'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v1+json'
conn.response :json, content_type: 'application/vnd.docker.distribution.manifest.v2+json'
+ end
- if options[:user] && options[:password]
- conn.request(:basic_auth, options[:user].to_s, options[:password].to_s)
- elsif options[:token]
- conn.request(:authorization, :bearer, options[:token].to_s)
+ def response_body(response, allow_redirect: false)
+ if allow_redirect && REDIRECT_CODES.include?(response.status)
+ response = redirect_response(response.headers['location'])
end
- conn.adapter :net_http
+ response.body if response && response.success?
+ end
+
+ def redirect_response(location)
+ return unless location
+
+ # We explicitly remove authorization token
+ faraday_blob.get(location) do |req|
+ req['Authorization'] = ''
+ end
end
- def response_body(response)
- response.body if response.success?
+ def faraday
+ @faraday ||= Faraday.new(@base_uri) do |conn|
+ initialize_connection(conn, @options)
+ accept_manifest(conn)
+ end
+ end
+
+ def faraday_blob
+ @faraday_blob ||= Faraday.new(@base_uri) do |conn|
+ initialize_connection(conn, @options)
+ end
end
end
end
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 708d01b95a1..59040199920 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -53,7 +53,7 @@ module ContainerRegistry
def config
return unless config_blob
- @config ||= ContainerRegistry::Config.new(self, config_blob)
+ @config ||= ContainerRegistry::Config.new(self, config_blob) if config_blob.data
end
def created_at
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 51e46da82cc..a4558d157c0 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,7 @@ module ExtractsPath
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
- @id = Addressable::URI.unescape(get_id)
+ @id = get_id
@ref, @path = extract_ref(@id)
@repo = @project.repository
if @options[:extended_sha1].blank?
@@ -119,6 +119,7 @@ module ExtractsPath
private
+ # overriden in subclasses, do not remove
def get_id
id = params[:id] || params[:ref]
id += "/" + params[:path] unless params[:path].blank?
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 831f1e635ba..a533bac2692 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -7,6 +7,7 @@ module Gitlab
module Access
class AccessDeniedError < StandardError; end
+ NO_ACCESS = 0
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -14,9 +15,10 @@ module Gitlab
OWNER = 50
# Branch protection settings
- PROTECTION_NONE = 0
- PROTECTION_DEV_CAN_PUSH = 1
- PROTECTION_FULL = 2
+ PROTECTION_NONE = 0
+ PROTECTION_DEV_CAN_PUSH = 1
+ PROTECTION_FULL = 2
+ PROTECTION_DEV_CAN_MERGE = 3
class << self
def values
@@ -54,6 +56,7 @@ module Gitlab
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
+ "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch." => PROTECTION_DEV_CAN_MERGE,
"Partially protected: Developers can push new commits, but cannot force push or delete the branch. Masters can do all of those." => PROTECTION_DEV_CAN_PUSH,
"Fully protected: Developers cannot push new commits, force push, or delete the branch. Only masters can do any of those." => PROTECTION_FULL,
}
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
deleted file mode 100644
index 04676fdb748..00000000000
--- a/lib/gitlab/akismet_helper.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module AkismetHelper
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def client_ip(env)
- env['action_dispatch.remote_ip'].to_s
- end
-
- def user_agent(env)
- env['HTTP_USER_AGENT']
- end
-
- def check_for_spam?(project, user)
- akismet_enabled? && !project.team.member?(user)
- end
-
- def is_spam?(environment, user, text)
- client = akismet_client
- ip_address = client_ip(environment)
- user_agent = user_agent(environment)
-
- params = {
- type: 'comment',
- text: text,
- created_at: DateTime.now,
- author: user.name,
- author_email: user.email,
- referrer: environment['HTTP_REFERER'],
- }
-
- begin
- is_spam, is_blatant = client.check(ip_address, user_agent, params)
- is_spam || is_blatant
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
- false
- end
- end
- end
-end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index db1704af75e..91f0270818a 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -10,13 +10,12 @@ module Gitlab
if valid_ci_request?(login, password, project)
result.type = :ci
- elsif result.user = find_with_user_password(login, password)
- result.type = :gitlab_or_ldap
- elsif result.user = oauth_access_token_check(login, password)
- result.type = :oauth
+ else
+ result = populate_result(login, password)
end
- rate_limit!(ip, success: !!result.user || (result.type == :ci), login: login)
+ success = result.user.present? || [:ci, :missing_personal_token].include?(result.type)
+ rate_limit!(ip, success: success, login: login)
result
end
@@ -76,10 +75,43 @@ module Gitlab
end
end
+ def populate_result(login, password)
+ result =
+ user_with_password_for_git(login, password) ||
+ oauth_access_token_check(login, password) ||
+ personal_access_token_check(login, password)
+
+ if result
+ result.type = nil unless result.user
+
+ if result.user && result.user.two_factor_enabled? && result.type == :gitlab_or_ldap
+ result.type = :missing_personal_token
+ end
+ end
+
+ result || Result.new
+ end
+
+ def user_with_password_for_git(login, password)
+ user = find_with_user_password(login, password)
+ Result.new(user, :gitlab_or_ldap) if user
+ end
+
def oauth_access_token_check(login, password)
if login == "oauth2" && password.present?
token = Doorkeeper::AccessToken.by_token(password)
- token && token.accessible? && User.find_by(id: token.resource_owner_id)
+ if token && token.accessible?
+ user = User.find_by(id: token.resource_owner_id)
+ Result.new(user, :oauth)
+ end
+ end
+ end
+
+ def personal_access_token_check(login, password)
+ if login && password
+ user = User.find_by_personal_access_token(password)
+ validation = User.by_login(login)
+ Result.new(user, :personal_token) if user == validation
end
end
end
diff --git a/lib/gitlab/award_emoji.rb b/lib/gitlab/award_emoji.rb
index c94bfc0e65f..39b43ab5489 100644
--- a/lib/gitlab/award_emoji.rb
+++ b/lib/gitlab/award_emoji.rb
@@ -1,24 +1,14 @@
module Gitlab
class AwardEmoji
CATEGORIES = {
- other: "Other",
objects: "Objects",
- places: "Places",
- travel_places: "Travel",
- emoticons: "Emoticons",
- objects_symbols: "Symbols",
+ travel: "Travel",
+ symbols: "Symbols",
nature: "Nature",
- celebration: "Celebration",
people: "People",
activity: "Activity",
flags: "Flags",
- food_drink: "Food"
- }.with_indifferent_access
-
- CATEGORY_ALIASES = {
- symbols: "objects_symbols",
- foods: "food_drink",
- travel: "travel_places"
+ food: "Food"
}.with_indifferent_access
def self.normalize_emoji_name(name)
@@ -35,7 +25,7 @@ module Gitlab
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
- category = CATEGORY_ALIASES[data["category"]] || data["category"]
+ category = data["category"]
@emoji_by_category[category] << data
end
@@ -57,9 +47,9 @@ module Gitlab
def self.aliases
@aliases ||=
begin
- json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json' )
- JSON.parse(File.read(json_path))
- end
+ json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
+ JSON.parse(File.read(json_path))
+ end
end
# Returns an Array of Emoji names and their asset URLs.
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
deleted file mode 100644
index 478f145bfed..00000000000
--- a/lib/gitlab/backend/grack_auth.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-module Grack
- class AuthSpawner
- def self.call(env)
- # Avoid issues with instance variables in Grack::Auth persisting across
- # requests by creating a new instance for each request.
- Auth.new({}).call(env)
- end
- end
-
- class Auth < Rack::Auth::Basic
- attr_accessor :user, :project, :env
-
- def call(env)
- @env = env
- @request = Rack::Request.new(env)
- @auth = Request.new(env)
-
- @ci = false
-
- # Need this patch due to the rails mount
- # Need this if under RELATIVE_URL_ROOT
- unless Gitlab.config.gitlab.relative_url_root.empty?
- # If website is mounted using relative_url_root need to remove it first
- @env['PATH_INFO'] = @request.path.sub(Gitlab.config.gitlab.relative_url_root, '')
- else
- @env['PATH_INFO'] = @request.path
- end
-
- @env['SCRIPT_NAME'] = ""
-
- auth!
-
- lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
- return lfs_response unless lfs_response.nil?
-
- if @user.nil? && !@ci
- unauthorized
- else
- render_not_found
- end
- end
-
- private
-
- def auth!
- return unless @auth.provided?
-
- return bad_request unless @auth.basic?
-
- # Authentication with username and password
- login, password = @auth.credentials
-
- # Allow authentication for GitLab CI service
- # if valid token passed
- if ci_request?(login, password)
- @ci = true
- return
- end
-
- @user = authenticate_user(login, password)
- end
-
- def ci_request?(login, password)
- matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
-
- if project && matched_login.present? && git_cmd == 'git-upload-pack'
- underscored_service = matched_login['s'].underscore
-
- if underscored_service == 'gitlab_ci'
- return project && project.valid_build_token?(password)
- elsif Service.available_services_names.include?(underscored_service)
- service_method = "#{underscored_service}_service"
- service = project.send(service_method)
-
- return service && service.activated? && service.valid_token?(password)
- end
- end
-
- false
- end
-
- def oauth_access_token_check(login, password)
- if login == "oauth2" && git_cmd == 'git-upload-pack' && password.present?
- token = Doorkeeper::AccessToken.by_token(password)
- token && token.accessible? && User.find_by(id: token.resource_owner_id)
- end
- end
-
- def authenticate_user(login, password)
- user = Gitlab::Auth.find_with_user_password(login, password)
-
- unless user
- user = oauth_access_token_check(login, password)
- end
-
- # If the user authenticated successfully, we reset the auth failure count
- # from Rack::Attack for that IP. A client may attempt to authenticate
- # with a username and blank password first, and only after it receives
- # a 401 error does it present a password. Resetting the count prevents
- # false positives from occurring.
- #
- # Otherwise, we let Rack::Attack know there was a failed authentication
- # attempt from this IP. This information is stored in the Rails cache
- # (Redis) and will be used by the Rack::Attack middleware to decide
- # whether to block requests from this IP.
- config = Gitlab.config.rack_attack.git_basic_auth
-
- if config.enabled
- if user
- # A successful login will reset the auth failure count from this IP
- Rack::Attack::Allow2Ban.reset(@request.ip, config)
- else
- banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
- # Unless the IP is whitelisted, return true so that Allow2Ban
- # increments the counter (stored in Rails.cache) for the IP
- if config.ip_whitelist.include?(@request.ip)
- false
- else
- true
- end
- end
-
- if banned
- Rails.logger.info "IP #{@request.ip} failed to login " \
- "as #{login} but has been temporarily banned from Git auth"
- end
- end
- end
-
- user
- end
-
- def git_cmd
- if @request.get?
- @request.params['service']
- elsif @request.post?
- File.basename(@request.path)
- else
- nil
- end
- end
-
- def project
- return @project if defined?(@project)
-
- @project = project_by_path(@request.path_info)
- end
-
- def project_by_path(path)
- if m = /^([\w\.\/-]+)\.git/.match(path).to_a
- path_with_namespace = m.last
- path_with_namespace.gsub!(/\.wiki$/, '')
-
- path_with_namespace[0] = '' if path_with_namespace.start_with?('/')
- Project.find_with_namespace(path_with_namespace)
- end
- end
-
- def render_not_found
- [404, { "Content-Type" => "text/plain" }, ["Not Found"]]
- end
- end
-end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 34e0143a82e..839a4fa30d5 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -60,16 +60,18 @@ module Gitlab
end
# Fork repository to new namespace
- # storage - project's storage path
+ # forked_from_storage - forked-from project's storage path
# path - project path with namespace
+ # forked_to_storage - forked-to project's storage path
# fork_namespace - namespace for forked project
#
# Ex.
- # fork_repository("/path/to/storage", "gitlab/gitlab-ci", "randx")
+ # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "randx")
#
- def fork_repository(storage, path, fork_namespace)
+ def fork_repository(forked_from_storage, path, forked_to_storage, fork_namespace)
Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project',
- storage, "#{path}.git", fork_namespace])
+ forked_from_storage, "#{path}.git", forked_to_storage,
+ fork_namespace])
end
# Remove repository from file system
diff --git a/lib/gitlab/badge/base.rb b/lib/gitlab/badge/base.rb
new file mode 100644
index 00000000000..909fa24fa90
--- /dev/null
+++ b/lib/gitlab/badge/base.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Badge
+ class Base
+ def entity
+ raise NotImplementedError
+ end
+
+ def status
+ raise NotImplementedError
+ end
+
+ def metadata
+ raise NotImplementedError
+ end
+
+ def template
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build.rb b/lib/gitlab/badge/build.rb
deleted file mode 100644
index e5e9fab3f5c..00000000000
--- a/lib/gitlab/badge/build.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module Gitlab
- module Badge
- ##
- # Build badge
- #
- class Build
- include Gitlab::Application.routes.url_helpers
- include ActionView::Helpers::AssetTagHelper
- include ActionView::Helpers::UrlHelper
-
- def initialize(project, ref)
- @project, @ref = project, ref
- @image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
- end
-
- def type
- 'image/svg+xml'
- end
-
- def data
- File.read(@image[:path])
- end
-
- def to_s
- @image[:name].sub(/\.svg$/, '')
- end
-
- def to_html
- link_to(image_tag(image_url, alt: 'build status'), link_url)
- end
-
- def to_markdown
- "[![build status](#{image_url})](#{link_url})"
- end
-
- def image_url
- build_namespace_project_badges_url(@project.namespace,
- @project, @ref, format: :svg)
- end
-
- def link_url
- namespace_project_commits_url(@project.namespace, @project, id: @ref)
- end
- end
- end
-end
diff --git a/lib/gitlab/badge/build/metadata.rb b/lib/gitlab/badge/build/metadata.rb
new file mode 100644
index 00000000000..f87a7b7942e
--- /dev/null
+++ b/lib/gitlab/badge/build/metadata.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module Badge
+ module Build
+ ##
+ # Class that describes build badge metadata
+ #
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
+ end
+
+ def title
+ 'build status'
+ end
+
+ def image_url
+ build_namespace_project_badges_url(@project.namespace,
+ @project, @ref, format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
new file mode 100644
index 00000000000..50aa45e5406
--- /dev/null
+++ b/lib/gitlab/badge/build/status.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Badge
+ module Build
+ ##
+ # Build status badge
+ #
+ class Status < Badge::Base
+ attr_reader :project, :ref
+
+ def initialize(project, ref)
+ @project = project
+ @ref = ref
+
+ @sha = @project.commit(@ref).try(:sha)
+ end
+
+ def entity
+ 'build'
+ end
+
+ def status
+ @project.pipelines
+ .where(sha: @sha, ref: @ref)
+ .status || 'unknown'
+ end
+
+ def metadata
+ @metadata ||= Build::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Build::Template.new(self)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/build/template.rb b/lib/gitlab/badge/build/template.rb
new file mode 100644
index 00000000000..2b95ddfcb53
--- /dev/null
+++ b/lib/gitlab/badge/build/template.rb
@@ -0,0 +1,47 @@
+module Gitlab
+ module Badge
+ module Build
+ ##
+ # Class that represents a build badge template.
+ #
+ # Template object will be passed to badge.svg.erb template.
+ #
+ class Template < Badge::Template
+ STATUS_COLOR = {
+ success: '#4c1',
+ failed: '#e05d44',
+ running: '#dfb317',
+ pending: '#dfb317',
+ canceled: '#9f9f9f',
+ skipped: '#9f9f9f',
+ unknown: '#9f9f9f'
+ }
+
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ @entity.to_s
+ end
+
+ def value_text
+ @status.to_s
+ end
+
+ def key_width
+ 38
+ end
+
+ def value_width
+ 54
+ end
+
+ def value_color
+ STATUS_COLOR[@status.to_sym] || STATUS_COLOR[:unknown]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/metadata.rb b/lib/gitlab/badge/coverage/metadata.rb
new file mode 100644
index 00000000000..53588185622
--- /dev/null
+++ b/lib/gitlab/badge/coverage/metadata.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that describes coverage badge metadata
+ #
+ class Metadata < Badge::Metadata
+ def initialize(badge)
+ @project = badge.project
+ @ref = badge.ref
+ @job = badge.job
+ end
+
+ def title
+ 'coverage report'
+ end
+
+ def image_url
+ coverage_namespace_project_badges_url(@project.namespace,
+ @project, @ref,
+ format: :svg)
+ end
+
+ def link_url
+ namespace_project_commits_url(@project.namespace, @project, id: @ref)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
new file mode 100644
index 00000000000..95d925dc7f3
--- /dev/null
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Test coverage report badge
+ #
+ class Report < Badge::Base
+ attr_reader :project, :ref, :job
+
+ def initialize(project, ref, job = nil)
+ @project = project
+ @ref = ref
+ @job = job
+
+ @pipeline = @project.pipelines
+ .latest_successful_for(@ref)
+ .first
+ end
+
+ def entity
+ 'coverage'
+ end
+
+ def status
+ @coverage ||= raw_coverage
+ return unless @coverage
+
+ @coverage.to_i
+ end
+
+ def metadata
+ @metadata ||= Coverage::Metadata.new(self)
+ end
+
+ def template
+ @template ||= Coverage::Template.new(self)
+ end
+
+ private
+
+ def raw_coverage
+ return unless @pipeline
+
+ if @job.blank?
+ @pipeline.coverage
+ else
+ @pipeline.builds
+ .find_by(name: @job)
+ .try(:coverage)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb
new file mode 100644
index 00000000000..06e0d084e9f
--- /dev/null
+++ b/lib/gitlab/badge/coverage/template.rb
@@ -0,0 +1,52 @@
+module Gitlab
+ module Badge
+ module Coverage
+ ##
+ # Class that represents a coverage badge template.
+ #
+ # Template object will be passed to badge.svg.erb template.
+ #
+ class Template < Badge::Template
+ STATUS_COLOR = {
+ good: '#4c1',
+ acceptable: '#a3c51c',
+ medium: '#dfb317',
+ low: '#e05d44',
+ unknown: '#9f9f9f'
+ }
+
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ @entity.to_s
+ end
+
+ def value_text
+ @status ? "#{@status}%" : 'unknown'
+ end
+
+ def key_width
+ 62
+ end
+
+ def value_width
+ @status ? 36 : 58
+ end
+
+ def value_color
+ case @status
+ when 95..100 then STATUS_COLOR[:good]
+ when 90..95 then STATUS_COLOR[:acceptable]
+ when 75..90 then STATUS_COLOR[:medium]
+ when 0..75 then STATUS_COLOR[:low]
+ else
+ STATUS_COLOR[:unknown]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/metadata.rb b/lib/gitlab/badge/metadata.rb
new file mode 100644
index 00000000000..548f85b78bb
--- /dev/null
+++ b/lib/gitlab/badge/metadata.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract class for badge metadata
+ #
+ class Metadata
+ include Gitlab::Application.routes.url_helpers
+ include ActionView::Helpers::AssetTagHelper
+ include ActionView::Helpers::UrlHelper
+
+ def initialize(badge)
+ @badge = badge
+ end
+
+ def to_html
+ link_to(image_tag(image_url, alt: title), link_url)
+ end
+
+ def to_markdown
+ "[![#{title}](#{image_url})](#{link_url})"
+ end
+
+ def title
+ raise NotImplementedError
+ end
+
+ def image_url
+ raise NotImplementedError
+ end
+
+ def link_url
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/badge/template.rb b/lib/gitlab/badge/template.rb
new file mode 100644
index 00000000000..bfeb0052642
--- /dev/null
+++ b/lib/gitlab/badge/template.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Badge
+ ##
+ # Abstract template class for badges
+ #
+ class Template
+ def initialize(badge)
+ @entity = badge.entity
+ @status = badge.status
+ end
+
+ def key_text
+ raise NotImplementedError
+ end
+
+ def value_text
+ raise NotImplementedError
+ end
+
+ def key_width
+ raise NotImplementedError
+ end
+
+ def value_width
+ raise NotImplementedError
+ end
+
+ def value_color
+ raise NotImplementedError
+ end
+
+ def key_color
+ '#555'
+ end
+
+ def key_text_anchor
+ key_width / 2
+ end
+
+ def value_text_anchor
+ key_width + (value_width / 2)
+ end
+
+ def width
+ key_width + value_width
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb
deleted file mode 100644
index 9f45aefda0f..00000000000
--- a/lib/gitlab/build_data_builder.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module Gitlab
- class BuildDataBuilder
- class << self
- def build(build)
- project = build.project
- commit = build.pipeline
- user = build.user
-
- data = {
- object_kind: 'build',
-
- ref: build.ref,
- tag: build.tag,
- before_sha: build.before_sha,
- sha: build.sha,
-
- # TODO: should this be not prefixed with build_?
- # Leaving this way to have backward compatibility
- build_id: build.id,
- build_name: build.name,
- build_stage: build.stage,
- build_status: build.status,
- build_started_at: build.started_at,
- build_finished_at: build.finished_at,
- build_duration: build.duration,
- build_allow_failure: build.allow_failure,
-
- # TODO: do we still need it?
- project_id: project.id,
- project_name: project.name_with_namespace,
-
- user: {
- id: user.try(:id),
- name: user.try(:name),
- email: user.try(:email),
- },
-
- commit: {
- id: commit.id,
- sha: commit.sha,
- message: commit.git_commit_message,
- author_name: commit.git_author_name,
- author_email: commit.git_author_email,
- status: commit.status,
- duration: commit.duration,
- started_at: commit.started_at,
- finished_at: commit.finished_at,
- },
-
- repository: {
- name: project.name,
- url: project.url_to_repo,
- description: project.description,
- homepage: project.web_url,
- git_http_url: project.http_url_to_repo,
- git_ssh_url: project.ssh_url_to_repo,
- visibility_level: project.visibility_level
- },
- }
-
- data
- end
- end
- end
-end
diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb
new file mode 100644
index 00000000000..95308aca95f
--- /dev/null
+++ b/lib/gitlab/changes_list.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ class ChangesList
+ include Enumerable
+
+ attr_reader :raw_changes
+
+ def initialize(changes)
+ @raw_changes = changes.kind_of?(String) ? changes.lines : changes
+ end
+
+ def each(&block)
+ changes.each(&block)
+ end
+
+ def changes
+ @changes ||= begin
+ @raw_changes.map do |change|
+ next if change.blank?
+ oldrev, newrev, ref = change.strip.split(' ')
+ { oldrev: oldrev, newrev: newrev, ref: ref }
+ end.compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
new file mode 100644
index 00000000000..4b32eb966aa
--- /dev/null
+++ b/lib/gitlab/checks/change_access.rb
@@ -0,0 +1,78 @@
+module Gitlab
+ module Checks
+ class ChangeAccess
+ attr_reader :user_access, :project
+
+ def initialize(change, user_access:, project:)
+ @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
+ @branch_name = Gitlab::Git.branch_name(@ref)
+ @user_access = user_access
+ @project = project
+ end
+
+ def exec
+ error = push_checks || tag_checks || protected_branch_checks
+
+ if error
+ GitAccessStatus.new(false, error)
+ else
+ GitAccessStatus.new(true)
+ end
+ end
+
+ protected
+
+ def protected_branch_checks
+ return unless project.protected_branch?(@branch_name)
+
+ if forced_push? && user_access.cannot_do_action?(:force_push_code_to_protected_branches)
+ return "You are not allowed to force push code to a protected branch on this project."
+ elsif Gitlab::Git.blank_ref?(@newrev) && user_access.cannot_do_action?(:remove_protected_branches)
+ return "You are not allowed to delete protected branches from this project."
+ end
+
+ if matching_merge_request?
+ if user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
+ return
+ else
+ "You are not allowed to merge code into protected branches on this project."
+ end
+ else
+ if user_access.can_push_to_branch?(@branch_name)
+ return
+ else
+ "You are not allowed to push code to protected branches on this project."
+ end
+ end
+ end
+
+ def tag_checks
+ tag_ref = Gitlab::Git.tag_name(@ref)
+
+ if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project)
+ "You are not allowed to change existing tags on this project."
+ end
+ end
+
+ def push_checks
+ if user_access.cannot_do_action?(:push_code)
+ "You are not allowed to push code to this project."
+ end
+ end
+
+ private
+
+ def protected_tag?(tag_name)
+ project.repository.tag_exists?(tag_name)
+ end
+
+ def forced_push?
+ Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
+ end
+
+ def matching_merge_request?
+ Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
new file mode 100644
index 00000000000..5fe86553bd0
--- /dev/null
+++ b/lib/gitlab/checks/force_push.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module Checks
+ class ForcePush
+ def self.force_push?(project, oldrev, newrev)
+ return false if project.empty_repo?
+
+ # Created or deleted branch
+ if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
+ false
+ else
+ missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev}))
+ missed_ref.present?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/matching_merge_request.rb b/lib/gitlab/checks/matching_merge_request.rb
new file mode 100644
index 00000000000..849848515da
--- /dev/null
+++ b/lib/gitlab/checks/matching_merge_request.rb
@@ -0,0 +1,18 @@
+module Gitlab
+ module Checks
+ class MatchingMergeRequest
+ def initialize(newrev, branch_name, project)
+ @newrev = newrev
+ @branch_name = branch_name
+ @project = project
+ end
+
+ def match?
+ @project.merge_requests
+ .with_state(:locked)
+ .where(in_progress_merge_commit_sha: @newrev, target_branch: @branch_name)
+ .exists?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index e6cc1529760..ae82c0db3f1 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -8,7 +8,7 @@ module Gitlab
# Temporary delegations that should be removed after refactoring
#
delegate :before_script, :image, :services, :after_script, :variables,
- :stages, :cache, to: :@global
+ :stages, :cache, :jobs, to: :@global
def initialize(config)
@config = Loader.new(config).load!
diff --git a/lib/gitlab/ci/config/node/artifacts.rb b/lib/gitlab/ci/config/node/artifacts.rb
new file mode 100644
index 00000000000..844bd2fe998
--- /dev/null
+++ b/lib/gitlab/ci/config/node/artifacts.rb
@@ -0,0 +1,35 @@
+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
new file mode 100644
index 00000000000..221b666f9f6
--- /dev/null
+++ b/lib/gitlab/ci/config/node/attributable.rb
@@ -0,0 +1,23 @@
+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/cache.rb b/lib/gitlab/ci/config/node/cache.rb
index cdf8ba2e35d..b4bda2841ac 100644
--- a/lib/gitlab/ci/config/node/cache.rb
+++ b/lib/gitlab/ci/config/node/cache.rb
@@ -8,6 +8,12 @@ module Gitlab
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.'
@@ -16,10 +22,6 @@ module Gitlab
node :paths, Node::Paths,
description: 'Specify which paths should be cached across builds.'
-
- validations do
- validates :config, allowed_keys: true
- end
end
end
end
diff --git a/lib/gitlab/ci/config/node/commands.rb b/lib/gitlab/ci/config/node/commands.rb
new file mode 100644
index 00000000000..d7657ae314b
--- /dev/null
+++ b/lib/gitlab/ci/config/node/commands.rb
@@ -0,0 +1,33 @@
+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
index 37936fc8242..2de82d40c9d 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -25,10 +25,14 @@ module Gitlab
private
- def create_node(key, factory)
- factory.with(value: @config[key], key: key, parent: self)
+ def compose!
+ self.class.nodes.each do |key, factory|
+ factory
+ .value(@config[key])
+ .with(key: key, parent: self)
- factory.create!
+ @entries[key] = factory.create!
+ end
end
class_methods do
@@ -36,24 +40,25 @@ module Gitlab
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end
- private
+ private # rubocop:disable Lint/UselessAccessModifier
- def node(symbol, entry_class, metadata)
- factory = Node::Factory.new(entry_class)
+ def node(key, node, metadata)
+ factory = Node::Factory.new(node)
.with(description: metadata[:description])
- (@nodes ||= {}).merge!(symbol.to_sym => factory)
+ (@nodes ||= {}).merge!(key.to_sym => factory)
end
def helpers(*nodes)
nodes.each do |symbol|
define_method("#{symbol}_defined?") do
- @nodes[symbol].try(:defined?)
+ @entries[symbol].specified? if @entries[symbol]
end
define_method("#{symbol}_value") do
- raise Entry::InvalidError unless valid?
- @nodes[symbol].try(:value)
+ return unless @entries[symbol] && @entries[symbol].valid?
+
+ @entries[symbol].value
end
alias_method symbol.to_sym, "#{symbol}_value".to_sym
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 9e79e170a4f..0c782c422b5 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -8,30 +8,31 @@ module Gitlab
class Entry
class InvalidError < StandardError; end
- attr_reader :config
+ attr_reader :config, :metadata
attr_accessor :key, :parent, :description
- def initialize(config)
+ def initialize(config, **metadata)
@config = config
- @nodes = {}
+ @metadata = metadata
+ @entries = {}
+
@validator = self.class.validator.new(self)
- @validator.validate
+ @validator.validate(:new)
end
def process!
- return if leaf?
return unless valid?
compose!
- process_nodes!
+ descendants.each(&:process!)
end
- def nodes
- @nodes.values
+ def leaf?
+ @entries.none?
end
- def leaf?
- self.class.nodes.none?
+ def descendants
+ @entries.values
end
def ancestors
@@ -43,27 +44,30 @@ module Gitlab
end
def errors
- @validator.messages + nodes.flat_map(&:errors)
+ @validator.messages + descendants.flat_map(&:errors)
end
def value
if leaf?
@config
else
- defined = @nodes.select { |_key, value| value.defined? }
- Hash[defined.map { |key, node| [key, node.value] }]
+ meaningful = @entries.select do |_key, value|
+ value.specified? && value.relevant?
+ end
+
+ Hash[meaningful.map { |key, entry| [key, entry.value] }]
end
end
- def defined?
+ def specified?
true
end
- def self.default
+ def relevant?
+ true
end
- def self.nodes
- {}
+ def self.default
end
def self.validator
@@ -73,17 +77,6 @@ module Gitlab
private
def compose!
- self.class.nodes.each do |key, essence|
- @nodes[key] = create_node(key, essence)
- end
- end
-
- def process_nodes!
- nodes.each(&:process!)
- end
-
- def create_node(key, essence)
- raise NotImplementedError
end
end
end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
index 5919a283283..707b052e6a8 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -10,35 +10,60 @@ module Gitlab
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 @attributes.has_key?(:value)
+ raise InvalidFactory unless defined?(@value)
- fabricate.tap do |entry|
- entry.key = @attributes[:key]
- entry.parent = @attributes[:parent]
- entry.description = @attributes[:description]
+ ##
+ # We assume that unspecified entry is undefined.
+ # See issue #18775.
+ #
+ if @value.nil?
+ Node::Undefined.new(
+ fabricate_undefined
+ )
+ else
+ fabricate(@node, @value)
end
end
private
- def fabricate
+ def fabricate_undefined
##
- # We assume that unspecified entry is undefined.
- # See issue #18775.
+ # If node has a default value we fabricate concrete node
+ # with default value.
#
- if @attributes[:value].nil?
- Node::Undefined.new(@node)
+ if @node.default.nil?
+ fabricate(Node::Null)
else
- @node.new(@attributes[:value])
+ 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
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
index f92e1eccbcf..ccd539fb003 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -34,10 +34,36 @@ module Gitlab
description: 'Configure caching between build jobs.'
helpers :before_script, :image, :services, :after_script,
- :variables, :stages, :types, :cache
+ :variables, :stages, :types, :cache, :jobs
- def stages
- stages_defined? ? stages_value : types_value
+ private
+
+ def compose!
+ super
+
+ compose_jobs!
+ compose_deprecated_entries!
+ end
+
+ 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
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden_job.rb
new file mode 100644
index 00000000000..073044b66f8
--- /dev/null
+++ b/lib/gitlab/ci/config/node/hidden_job.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # Entry that represents a hidden CI/CD job.
+ #
+ class HiddenJob < Entry
+ include Validatable
+
+ validations do
+ validates :config, type: Hash
+ validates :config, presence: true
+ end
+
+ def relevant?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
new file mode 100644
index 00000000000..e84737acbb9
--- /dev/null
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -0,0 +1,123 @@
+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, :environment, :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 :environment,
+ type: {
+ with: String,
+ message: Gitlab::Regex.environment_name_regex_message }
+ validates :environment,
+ format: {
+ with: Gitlab::Regex.environment_name_regex,
+ message: Gitlab::Regex.environment_name_regex_message }
+
+ validates :dependencies, array_of_strings: true
+ end
+ end
+
+ node :before_script, Script,
+ description: 'Global before script overridden in this job.'
+
+ node :script, Commands,
+ description: 'Commands that will be executed in this job.'
+
+ node :stage, Stage,
+ description: 'Pipeline stage this job will be executed into.'
+
+ node :type, Stage,
+ description: 'Deprecated: stage this job will be executed into.'
+
+ node :after_script, Script,
+ description: 'Commands that will be executed when finishing job.'
+
+ node :cache, Cache,
+ description: 'Cache definition for this job.'
+
+ node :image, Image,
+ description: 'Image that will be used to execute this job.'
+
+ node :services, Services,
+ description: 'Services that will be used to execute this job.'
+
+ node :only, Trigger,
+ description: 'Refs policy this job will be executed for.'
+
+ node :except, Trigger,
+ description: 'Refs policy this job will be executed for.'
+
+ node :variables, Variables,
+ description: 'Environment variables available for this job.'
+
+ node :artifacts, Artifacts,
+ description: 'Artifacts configuration for this job.'
+
+ helpers :before_script, :script, :stage, :type, :after_script,
+ :cache, :image, :services, :only, :except, :variables,
+ :artifacts
+
+ def name
+ @metadata[:name]
+ end
+
+ def value
+ @config.merge(to_hash.compact)
+ end
+
+ private
+
+ def to_hash
+ { name: name,
+ before_script: before_script,
+ script: script,
+ image: image,
+ services: services,
+ stage: stage,
+ cache: cache,
+ only: only,
+ except: except,
+ variables: variables_defined? ? variables : nil,
+ artifacts: artifacts,
+ after_script: after_script }
+ end
+
+ def compose!
+ super
+
+ if type_defined? && !stage_defined?
+ @entries[:stage] = @entries[:type]
+ end
+
+ @entries.delete(:type)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
new file mode 100644
index 00000000000..51683c82ceb
--- /dev/null
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -0,0 +1,48 @@
+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
+
+ private
+
+ def compose!
+ @config.each do |name, config|
+ node = hidden?(name) ? Node::HiddenJob : 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
+ 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
index 4d9a508796a..0c291efe6a5 100644
--- a/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
+++ b/lib/gitlab/ci/config/node/legacy_validation_helpers.rb
@@ -41,10 +41,6 @@ module Gitlab
false
end
- def validate_environment(value)
- value.is_a?(String) && value =~ Gitlab::Regex.environment_name_regex
- end
-
def validate_boolean(value)
value.in?([true, false])
end
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
new file mode 100644
index 00000000000..88a5f53f13c
--- /dev/null
+++ b/lib/gitlab/ci/config/node/null.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents an undefined node.
+ #
+ # Implements the Null Object pattern.
+ #
+ class Null < Entry
+ 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/stage.rb b/lib/gitlab/ci/config/node/stage.rb
new file mode 100644
index 00000000000..cbc97641f5a
--- /dev/null
+++ b/lib/gitlab/ci/config/node/stage.rb
@@ -0,0 +1,22 @@
+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/trigger.rb b/lib/gitlab/ci/config/node/trigger.rb
new file mode 100644
index 00000000000..d8b31975088
--- /dev/null
+++ b/lib/gitlab/ci/config/node/trigger.rb
@@ -0,0 +1,26 @@
+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
index 699605e1e3a..45fef8c3ae5 100644
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ b/lib/gitlab/ci/config/node/undefined.rb
@@ -3,24 +3,13 @@ module Gitlab
class Config
module Node
##
- # This class represents an undefined entry node.
+ # This class represents an unspecified entry node.
#
- # It takes original entry class as configuration and returns default
- # value of original entry as self value.
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
#
- #
- class Undefined < Entry
- include Validatable
-
- validations do
- validates :config, type: Class
- end
-
- def value
- @config.default
- end
-
- def defined?
+ class Undefined < SimpleDelegator
+ def specified?
false
end
end
diff --git a/lib/gitlab/ci/config/node/validatable.rb b/lib/gitlab/ci/config/node/validatable.rb
index f6e2896dfb2..085e6e988d1 100644
--- a/lib/gitlab/ci/config/node/validatable.rb
+++ b/lib/gitlab/ci/config/node/validatable.rb
@@ -7,13 +7,11 @@ module Gitlab
class_methods do
def validator
- validator = Class.new(Node::Validator)
-
- if defined?(@validations)
- @validations.each { |rules| validator.class_eval(&rules) }
+ @validator ||= Class.new(Node::Validator).tap do |validator|
+ if defined?(@validations)
+ @validations.each { |rules| validator.class_eval(&rules) }
+ end
end
-
- validator
end
private
diff --git a/lib/gitlab/ci/config/node/validator.rb b/lib/gitlab/ci/config/node/validator.rb
index 758a6cf4356..43c7e102b50 100644
--- a/lib/gitlab/ci/config/node/validator.rb
+++ b/lib/gitlab/ci/config/node/validator.rb
@@ -21,18 +21,19 @@ module Gitlab
'Validator'
end
- def unknown_keys
- return [] unless config.is_a?(Hash)
-
- config.keys - @node.class.nodes.keys
- end
-
private
def location
predecessors = ancestors.map(&:key).compact
- current = key || @node.class.name.demodulize.underscore
- predecessors.append(current).join(':')
+ predecessors.append(key_name).join(':')
+ end
+
+ def key_name
+ if key.blank?
+ @node.class.name.demodulize.underscore.humanize
+ else
+ key
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/validators.rb b/lib/gitlab/ci/config/node/validators.rb
index 7b2f57990b5..e20908ad3cb 100644
--- a/lib/gitlab/ci/config/node/validators.rb
+++ b/lib/gitlab/ci/config/node/validators.rb
@@ -5,10 +5,11 @@ module Gitlab
module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- if record.unknown_keys.any?
- unknown_list = record.unknown_keys.join(', ')
- record.errors.add(:config,
- "contains unknown keys: #{unknown_list}")
+ 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
@@ -33,6 +34,16 @@ module Gitlab
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
@@ -49,7 +60,8 @@ module Gitlab
raise unless type.is_a?(Class)
unless value.is_a?(type)
- record.errors.add(attribute, "should be a #{type.name}")
+ message = options[:message] || "should be a #{type.name}"
+ record.errors.add(attribute, message)
end
end
end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index 9bef9037ad6..58f86abc5c4 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -22,7 +22,9 @@ module Gitlab
@extractor.analyze(closing_statements.join(" "))
- @extractor.issues
+ @extractor.issues.reject do |issue|
+ @extractor.project.forked_from?(issue.project) # Don't extract issues on original project
+ end
end
end
end
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
new file mode 100644
index 00000000000..dff9e29c6a5
--- /dev/null
+++ b/lib/gitlab/conflict/file.rb
@@ -0,0 +1,197 @@
+module Gitlab
+ module Conflict
+ class File
+ include Gitlab::Routing.url_helpers
+ include IconsHelper
+
+ class MissingResolution < StandardError
+ end
+
+ CONTEXT_LINES = 3
+
+ attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository
+
+ def initialize(merge_file_result, conflict, merge_request:)
+ @merge_file_result = merge_file_result
+ @their_path = conflict[:theirs][:path]
+ @our_path = conflict[:ours][:path]
+ @our_mode = conflict[:ours][:mode]
+ @merge_request = merge_request
+ @repository = merge_request.project.repository
+ @match_line_headers = {}
+ end
+
+ # Array of Gitlab::Diff::Line objects
+ def lines
+ @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data],
+ our_path: our_path,
+ their_path: their_path,
+ parent_file: self)
+ end
+
+ def resolve_lines(resolution)
+ section_id = nil
+
+ lines.map do |line|
+ unless line.type
+ section_id = nil
+ next line
+ end
+
+ section_id ||= line_code(line)
+
+ case resolution[section_id]
+ when 'head'
+ next unless line.type == 'new'
+ when 'origin'
+ next unless line.type == 'old'
+ else
+ raise MissingResolution, "Missing resolution for section ID: #{section_id}"
+ end
+
+ line
+ end.compact
+ end
+
+ def highlight_lines!
+ their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n")
+ our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n")
+
+ their_highlight = Gitlab::Highlight.highlight(their_path, their_file, repository: repository).lines
+ our_highlight = Gitlab::Highlight.highlight(our_path, our_file, repository: repository).lines
+
+ lines.each do |line|
+ if line.type == 'old'
+ line.rich_text = their_highlight[line.old_line - 1].try(:html_safe)
+ else
+ line.rich_text = our_highlight[line.new_line - 1].try(:html_safe)
+ end
+ end
+ end
+
+ def sections
+ return @sections if @sections
+
+ chunked_lines = lines.chunk { |line| line.type.nil? }.to_a
+ match_line = nil
+
+ sections_count = chunked_lines.size
+
+ @sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
+ section = nil
+
+ # We need to reduce context sections to CONTEXT_LINES. Conflict sections are
+ # always shown in full.
+ if no_conflict
+ conflict_before = i > 0
+ conflict_after = (sections_count - i) > 1
+
+ if conflict_before && conflict_after
+ # Create a gap in a long context section.
+ if lines.length > CONTEXT_LINES * 2
+ head_lines = lines.first(CONTEXT_LINES)
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Ensure any existing match line has text for all lines up to the last
+ # line of its context.
+ update_match_line_text(match_line, head_lines.last)
+
+ # Insert a new match line after the created gap.
+ match_line = create_match_line(tail_lines.first)
+
+ section = [
+ { conflict: false, lines: head_lines },
+ { conflict: false, lines: tail_lines.unshift(match_line) }
+ ]
+ end
+ elsif conflict_after
+ tail_lines = lines.last(CONTEXT_LINES)
+
+ # Create a gap and insert a match line at the start.
+ if lines.length > tail_lines.length
+ match_line = create_match_line(tail_lines.first)
+
+ tail_lines.unshift(match_line)
+ end
+
+ lines = tail_lines
+ elsif conflict_before
+ # We're at the end of the file (no conflicts after), so just remove extra
+ # trailing lines.
+ lines = lines.first(CONTEXT_LINES)
+ end
+ end
+
+ # We want to update the match line's text every time unless we've already
+ # created a gap and its corresponding match line.
+ update_match_line_text(match_line, lines.last) unless section
+
+ section ||= { conflict: !no_conflict, lines: lines }
+ section[:id] = line_code(lines.first) unless no_conflict
+ section
+ end
+ end
+
+ def line_code(line)
+ Gitlab::Diff::LineCode.generate(our_path, line.new_pos, line.old_pos)
+ end
+
+ def create_match_line(line)
+ Gitlab::Diff::Line.new('', 'match', line.index, line.old_pos, line.new_pos)
+ end
+
+ # Any line beginning with a letter, an underscore, or a dollar can be used in a
+ # match line header. Only context sections can contain match lines, as match lines
+ # have to exist in both versions of the file.
+ def find_match_line_header(index)
+ return @match_line_headers[index] if @match_line_headers.key?(index)
+
+ @match_line_headers[index] = begin
+ if index >= 0
+ line = lines[index]
+
+ if line.type.nil? && line.text.match(/\A[A-Za-z$_]/)
+ " #{line.text}"
+ else
+ find_match_line_header(index - 1)
+ end
+ end
+ end
+ end
+
+ # Set the match line's text for the current line. A match line takes its start
+ # position and context header (where present) from itself, and its end position from
+ # the line passed in.
+ def update_match_line_text(match_line, line)
+ return unless match_line
+
+ header = find_match_line_header(match_line.index - 1)
+
+ match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}"
+ end
+
+ def as_json(opts = nil)
+ {
+ old_path: their_path,
+ new_path: our_path,
+ blob_icon: file_type_icon_class('file', our_mode, our_path),
+ blob_path: namespace_project_blob_path(merge_request.project.namespace,
+ merge_request.project,
+ ::File.join(merge_request.diff_refs.head_sha, our_path)),
+ sections: sections
+ }
+ end
+
+ # Don't try to print merge_request or repository.
+ def inspect
+ instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable|
+ value = instance_variable_get("@#{instance_variable}")
+
+ "#{instance_variable}=\"#{value}\""
+ end
+
+ "#<#{self.class} #{instance_variables.join(' ')}>"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
new file mode 100644
index 00000000000..bbd0427a2c8
--- /dev/null
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module Conflict
+ class FileCollection
+ class ConflictSideMissing < StandardError
+ end
+
+ attr_reader :merge_request, :our_commit, :their_commit
+
+ def initialize(merge_request)
+ @merge_request = merge_request
+ @our_commit = merge_request.source_branch_head.raw.raw_commit
+ @their_commit = merge_request.target_branch_head.raw.raw_commit
+ end
+
+ def repository
+ merge_request.project.repository
+ end
+
+ def merge_index
+ @merge_index ||= repository.rugged.merge_commits(our_commit, their_commit)
+ end
+
+ def files
+ @files ||= merge_index.conflicts.map do |conflict|
+ raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
+
+ Gitlab::Conflict::File.new(merge_index.merge_file(conflict[:ours][:path]),
+ conflict,
+ merge_request: merge_request)
+ end
+ end
+
+ def as_json(opts = nil)
+ {
+ target_branch: merge_request.target_branch,
+ source_branch: merge_request.source_branch,
+ commit_sha: merge_request.diff_head_sha,
+ commit_message: default_commit_message,
+ files: files
+ }
+ end
+
+ def default_commit_message
+ conflict_filenames = merge_index.conflicts.map do |conflict|
+ "# #{conflict[:ours][:path]}"
+ end
+
+ <<EOM.chomp
+Merge branch '#{merge_request.target_branch}' into '#{merge_request.source_branch}'
+
+# Conflicts:
+#{conflict_filenames.join("\n")}
+EOM
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
new file mode 100644
index 00000000000..2d4d55daeeb
--- /dev/null
+++ b/lib/gitlab/conflict/parser.rb
@@ -0,0 +1,71 @@
+module Gitlab
+ module Conflict
+ class Parser
+ class ParserError < StandardError
+ end
+
+ class UnexpectedDelimiter < ParserError
+ end
+
+ class MissingEndDelimiter < ParserError
+ end
+
+ class UnmergeableFile < ParserError
+ end
+
+ class UnsupportedEncoding < ParserError
+ end
+
+ def parse(text, our_path:, their_path:, parent_file: nil)
+ raise UnmergeableFile if text.blank? # Typically a binary file
+ raise UnmergeableFile if text.length > 102400
+
+ begin
+ text.to_json
+ rescue Encoding::UndefinedConversionError
+ raise UnsupportedEncoding
+ end
+
+ line_obj_index = 0
+ line_old = 1
+ line_new = 1
+ type = nil
+ lines = []
+ conflict_start = "<<<<<<< #{our_path}"
+ conflict_middle = '======='
+ conflict_end = ">>>>>>> #{their_path}"
+
+ text.each_line.map do |line|
+ full_line = line.delete("\n")
+
+ if full_line == conflict_start
+ raise UnexpectedDelimiter unless type.nil?
+
+ type = 'new'
+ elsif full_line == conflict_middle
+ raise UnexpectedDelimiter unless type == 'new'
+
+ type = 'old'
+ elsif full_line == conflict_end
+ raise UnexpectedDelimiter unless type == 'old'
+
+ type = nil
+ elsif line[0] == '\\'
+ type = 'nonewline'
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ else
+ lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new, parent_file: parent_file)
+ line_old += 1 if type != 'new'
+ line_new += 1 if type != 'old'
+
+ line_obj_index += 1
+ end
+ end
+
+ raise MissingEndDelimiter unless type.nil?
+
+ lines
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 9dc2602867e..bd681f03173 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -23,7 +23,6 @@ module Gitlab
dates.each do |date|
date_id = date.to_time.to_i.to_s
- @timestamps[date_id] = 0
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index ffc1814b29d..12fbb78c53e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -30,6 +30,7 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
+ koding_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
@@ -39,8 +40,8 @@ module Gitlab
session_expire_delay: Settings.gitlab['session_expire_delay'],
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
- restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ domain_whitelist: Settings.gitlab['domain_whitelist'],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
new file mode 100644
index 00000000000..6548e6475c6
--- /dev/null
+++ b/lib/gitlab/data_builder/build.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module DataBuilder
+ module Build
+ extend self
+
+ def build(build)
+ project = build.project
+ commit = build.pipeline
+ user = build.user
+
+ data = {
+ object_kind: 'build',
+
+ ref: build.ref,
+ tag: build.tag,
+ before_sha: build.before_sha,
+ sha: build.sha,
+
+ # TODO: should this be not prefixed with build_?
+ # Leaving this way to have backward compatibility
+ build_id: build.id,
+ build_name: build.name,
+ build_stage: build.stage,
+ build_status: build.status,
+ build_started_at: build.started_at,
+ build_finished_at: build.finished_at,
+ build_duration: build.duration,
+ build_allow_failure: build.allow_failure,
+
+ # TODO: do we still need it?
+ project_id: project.id,
+ project_name: project.name_with_namespace,
+
+ user: {
+ id: user.try(:id),
+ name: user.try(:name),
+ email: user.try(:email),
+ },
+
+ commit: {
+ id: commit.id,
+ sha: commit.sha,
+ message: commit.git_commit_message,
+ author_name: commit.git_author_name,
+ author_email: commit.git_author_email,
+ status: commit.status,
+ duration: commit.duration,
+ started_at: commit.started_at,
+ finished_at: commit.finished_at,
+ },
+
+ repository: {
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url,
+ git_http_url: project.http_url_to_repo,
+ git_ssh_url: project.ssh_url_to_repo,
+ visibility_level: project.visibility_level
+ },
+ }
+
+ data
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/note.rb b/lib/gitlab/data_builder/note.rb
new file mode 100644
index 00000000000..50fea1232af
--- /dev/null
+++ b/lib/gitlab/data_builder/note.rb
@@ -0,0 +1,75 @@
+module Gitlab
+ module DataBuilder
+ module Note
+ extend self
+
+ # Produce a hash of post-receive data
+ #
+ # For all notes:
+ #
+ # data = {
+ # object_kind: "note",
+ # user: {
+ # name: String,
+ # username: String,
+ # avatar_url: String
+ # }
+ # project_id: Integer,
+ # repository: {
+ # name: String,
+ # url: String,
+ # description: String,
+ # homepage: String,
+ # }
+ # object_attributes: {
+ # <hook data for note>
+ # }
+ # <note-specific data>: {
+ # }
+ # note-specific data is a hash with one of the following keys and contains
+ # the hook data for that type.
+ # - commit
+ # - issue
+ # - merge_request
+ # - snippet
+ #
+ def build(note, user)
+ project = note.project
+ data = build_base_data(project, user, note)
+
+ if note.for_commit?
+ data[:commit] = build_data_for_commit(project, user, note)
+ elsif note.for_issue?
+ data[:issue] = note.noteable.hook_attrs
+ elsif note.for_merge_request?
+ data[:merge_request] = note.noteable.hook_attrs
+ elsif note.for_snippet?
+ data[:snippet] = note.noteable.hook_attrs
+ end
+
+ data
+ end
+
+ def build_base_data(project, user, note)
+ base_data = {
+ object_kind: "note",
+ user: user.hook_attrs,
+ project_id: project.id,
+ project: project.hook_attrs,
+ object_attributes: note.hook_attrs,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note)
+ base_data
+ end
+
+ def build_data_for_commit(project, user, note)
+ # commit_id is the SHA hash
+ commit = project.commit(note.commit_id)
+ commit.hook_attrs
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/pipeline.rb b/lib/gitlab/data_builder/pipeline.rb
new file mode 100644
index 00000000000..06a783ebc1c
--- /dev/null
+++ b/lib/gitlab/data_builder/pipeline.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module DataBuilder
+ module Pipeline
+ extend self
+
+ def build(pipeline)
+ {
+ object_kind: 'pipeline',
+ object_attributes: hook_attrs(pipeline),
+ user: pipeline.user.try(:hook_attrs),
+ project: pipeline.project.hook_attrs(backward: false),
+ commit: pipeline.commit.try(:hook_attrs),
+ builds: pipeline.builds.map(&method(:build_hook_attrs))
+ }
+ end
+
+ def hook_attrs(pipeline)
+ {
+ id: pipeline.id,
+ ref: pipeline.ref,
+ tag: pipeline.tag,
+ sha: pipeline.sha,
+ before_sha: pipeline.before_sha,
+ status: pipeline.status,
+ stages: pipeline.stages,
+ created_at: pipeline.created_at,
+ finished_at: pipeline.finished_at,
+ duration: pipeline.duration
+ }
+ end
+
+ def build_hook_attrs(build)
+ {
+ id: build.id,
+ stage: build.stage,
+ name: build.name,
+ status: build.status,
+ created_at: build.created_at,
+ started_at: build.started_at,
+ finished_at: build.finished_at,
+ when: build.when,
+ manual: build.manual?,
+ user: build.user.try(:hook_attrs),
+ runner: build.runner && runner_hook_attrs(build.runner),
+ artifacts_file: {
+ filename: build.artifacts_file.filename,
+ size: build.artifacts_size
+ }
+ }
+ end
+
+ def runner_hook_attrs(runner)
+ {
+ id: runner.id,
+ description: runner.description,
+ active: runner.active?,
+ is_shared: runner.is_shared?
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
new file mode 100644
index 00000000000..4f81863da35
--- /dev/null
+++ b/lib/gitlab/data_builder/push.rb
@@ -0,0 +1,95 @@
+module Gitlab
+ module DataBuilder
+ module Push
+ extend self
+
+ # Produce a hash of post-receive data
+ #
+ # data = {
+ # before: String,
+ # after: String,
+ # ref: String,
+ # user_id: String,
+ # user_name: String,
+ # user_email: String
+ # project_id: String,
+ # repository: {
+ # name: String,
+ # url: String,
+ # description: String,
+ # homepage: String,
+ # },
+ # commits: Array,
+ # total_commits_count: Fixnum
+ # }
+ #
+ def build(project, user, oldrev, newrev, ref, commits = [], message = nil)
+ commits = Array(commits)
+
+ # Total commits count
+ commits_count = commits.size
+
+ # Get latest 20 commits ASC
+ commits_limited = commits.last(20)
+
+ # For performance purposes maximum 20 latest commits
+ # will be passed as post receive hook data.
+ commit_attrs = commits_limited.map do |commit|
+ commit.hook_attrs(with_changed_files: true)
+ end
+
+ type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
+
+ # Hash to be passed as post_receive_data
+ data = {
+ object_kind: type,
+ event_name: type,
+ before: oldrev,
+ after: newrev,
+ ref: ref,
+ checkout_sha: checkout_sha(project.repository, newrev, ref),
+ message: message,
+ user_id: user.id,
+ user_name: user.name,
+ user_email: user.email,
+ user_avatar: user.avatar_url,
+ project_id: project.id,
+ project: project.hook_attrs,
+ commits: commit_attrs,
+ total_commits_count: commits_count,
+ # DEPRECATED
+ repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
+ :git_http_url, :git_ssh_url, :visibility_level)
+ }
+
+ data
+ end
+
+ # This method provide a sample data generated with
+ # existing project and commits to test webhooks
+ def build_sample(project, user)
+ commits = project.repository.commits(project.default_branch, limit: 3)
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
+ build(project, user, commits.last.id, commits.first.id, ref, commits)
+ end
+
+ def checkout_sha(repository, newrev, ref)
+ # Checkout sha is nil when we remove branch or tag
+ return if Gitlab::Git.blank_ref?(newrev)
+
+ # Find sha for tag, except when it was deleted.
+ if Gitlab::Git.tag_ref?(ref)
+ tag_name = Gitlab::Git.ref_name(ref)
+ tag = repository.find_tag(tag_name)
+
+ if tag
+ commit = repository.commit(tag.target)
+ commit.try(:sha)
+ end
+ else
+ newrev
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 078609c86f1..55b8f888d53 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -55,12 +55,12 @@ module Gitlab
end
end
- private
-
def self.connection
ActiveRecord::Base.connection
end
+ private_class_method :connection
+
def self.database_version
row = connection.execute("SELECT VERSION()").first
@@ -70,5 +70,7 @@ module Gitlab
row.first
end
end
+
+ private_class_method :database_version
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index dec20d8659b..927f9dad20b 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -20,11 +20,19 @@ module Gitlab
if Database.postgresql?
options = options.merge({ algorithm: :concurrently })
+ disable_statement_timeout
end
add_index(table_name, column_name, options)
end
+ # Long-running migrations may take more than the timeout allowed by
+ # the database. Disable the session's statement timeout to ensure
+ # migrations don't get killed prematurely. (PostgreSQL only)
+ def disable_statement_timeout
+ ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') if Database.postgresql?
+ end
+
# Updates the value of a column in batches.
#
# This method updates the table in batches of 5% of the total row count.
@@ -133,6 +141,8 @@ module Gitlab
'in the body of your migration class'
end
+ disable_statement_timeout
+
transaction do
add_column(table, column, type, default: nil)
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index b0c50edba59..e47df508ca2 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -5,7 +5,7 @@ module Gitlab
delegate :new_file, :deleted_file, :renamed_file,
:old_path, :new_path, :a_mode, :b_mode,
- :submodule?, :too_large?, to: :diff, prefix: false
+ :submodule?, :too_large?, :collapsed?, to: :diff, prefix: false
def initialize(diff, repository:, diff_refs: nil)
@diff = diff
@@ -63,15 +63,18 @@ module Gitlab
diff_refs.try(:head_sha)
end
+ attr_writer :highlighted_diff_lines
+
# Array of Gitlab::Diff::Line objects
def diff_lines
- @lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
+ @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
end
def highlighted_diff_lines
@highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end
+ # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
def parallel_diff_lines
@parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
new file mode 100644
index 00000000000..2b9fc65b985
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Diff
+ module FileCollection
+ class Base
+ attr_reader :project, :diff_options, :diff_view, :diff_refs
+
+ delegate :count, :size, :real_size, to: :diff_files
+
+ def self.default_options
+ ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false)
+ end
+
+ def initialize(diffable, project:, diff_options: nil, diff_refs: nil)
+ diff_options = self.class.default_options.merge(diff_options || {})
+
+ @diffable = diffable
+ @diffs = diffable.raw_diffs(diff_options)
+ @project = project
+ @diff_options = diff_options
+ @diff_refs = diff_refs
+ end
+
+ def diff_files
+ @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ end
+
+ private
+
+ def decorate_diff!(diff)
+ Gitlab::Diff::File.new(diff, repository: project.repository, diff_refs: diff_refs)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/commit.rb b/lib/gitlab/diff/file_collection/commit.rb
new file mode 100644
index 00000000000..4dc297ec036
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/commit.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Diff
+ module FileCollection
+ class Commit < Base
+ def initialize(commit, diff_options:)
+ super(commit,
+ project: commit.project,
+ diff_options: diff_options,
+ diff_refs: commit.diff_refs)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/compare.rb b/lib/gitlab/diff/file_collection/compare.rb
new file mode 100644
index 00000000000..20d8f891cc3
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/compare.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Diff
+ module FileCollection
+ class Compare < Base
+ def initialize(compare, project:, diff_options:, diff_refs: nil)
+ super(compare,
+ project: project,
+ diff_options: diff_options,
+ diff_refs: diff_refs)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
new file mode 100644
index 00000000000..36348b33943
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Diff
+ module FileCollection
+ class MergeRequestDiff < Base
+ def initialize(merge_request_diff, diff_options:)
+ @merge_request_diff = merge_request_diff
+
+ super(merge_request_diff,
+ project: merge_request_diff.project,
+ diff_options: diff_options,
+ diff_refs: merge_request_diff.diff_refs)
+ end
+
+ def diff_files
+ super.tap { |_| store_highlight_cache }
+ end
+
+ private
+
+ # 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?
+ diff_file
+ end
+
+ def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
+ diff_file.highlighted_diff_lines = cache_diff_lines.map do |line|
+ Gitlab::Diff::Line.init_from_hash(line)
+ end
+ end
+
+ #
+ # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted)
+ # for the highlighted ones, so we just skip their execution.
+ # If the highlighted diff files lines are not cached we calculate and cache them.
+ #
+ # The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of
+ # hashes that represent serialized diff lines.
+ #
+ def cache_highlight!(diff_file)
+ file_path = diff_file.file_path
+
+ if highlight_cache[file_path]
+ highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path])
+ else
+ highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash)
+ end
+ end
+
+ def highlight_cache
+ return @highlight_cache if defined?(@highlight_cache)
+
+ @highlight_cache = Rails.cache.read(cache_key) || {}
+ @highlight_cache_was_empty = @highlight_cache.empty?
+ @highlight_cache
+ end
+
+ def store_highlight_cache
+ Rails.cache.write(cache_key, highlight_cache) if @highlight_cache_was_empty
+ end
+
+ def cacheable?
+ @merge_request_diff.present?
+ end
+
+ def cache_key
+ [@merge_request_diff, 'highlighted-diff-files', diff_options]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/highlight.rb b/lib/gitlab/diff/highlight.rb
index 649a265a02c..9ea976e18fa 100644
--- a/lib/gitlab/diff/highlight.rb
+++ b/lib/gitlab/diff/highlight.rb
@@ -40,8 +40,6 @@ module Gitlab
def highlight_line(diff_line)
return unless diff_file && diff_file.diff_refs
- line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
-
rich_line =
if diff_line.unchanged? || diff_line.added?
new_lines[diff_line.new_pos - 1]
@@ -51,7 +49,10 @@ module Gitlab
# Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content.
- "#{line_prefix}#{rich_line}".html_safe if rich_line
+ if rich_line
+ line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
+ "#{line_prefix}#{rich_line}".html_safe
+ end
end
def inline_diffs
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 789c14518b0..55708d42161 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -1,27 +1,23 @@
module Gitlab
module Diff
class InlineDiff
- attr_accessor :old_line, :new_line, :offset
-
- def self.for_lines(lines)
- local_edit_indexes = self.find_local_edits(lines)
-
- inline_diffs = []
+ # Regex to find a run of deleted lines followed by the same number of added lines
+ LINE_PAIRS_PATTERN = %r{
+ # Runs start at the beginning of the string (the first line) or after a space (for an unchanged line)
+ (?:\A|\s)
+
+ # This matches a number of `-`s followed by the same number of `+`s through recursion
+ (?<del_ins>
+ -
+ \g<del_ins>?
+ \+
+ )
+
+ # Runs end at the end of the string (the last line) or before a space (for an unchanged line)
+ (?=\s|\z)
+ }x.freeze
- local_edit_indexes.each do |index|
- old_index = index
- new_index = index + 1
- old_line = lines[old_index]
- new_line = lines[new_index]
-
- old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs
-
- inline_diffs[old_index] = old_diffs
- inline_diffs[new_index] = new_diffs
- end
-
- inline_diffs
- end
+ attr_accessor :old_line, :new_line, :offset
def initialize(old_line, new_line, offset: 0)
@old_line = old_line[offset..-1]
@@ -49,22 +45,54 @@ module Gitlab
[old_diffs, new_diffs]
end
- private
+ class << self
+ def for_lines(lines)
+ changed_line_pairs = find_changed_line_pairs(lines)
+
+ inline_diffs = []
- def self.find_local_edits(lines)
- line_prefixes = lines.map { |line| line.match(/\A([+-])/) ? $1 : ' ' }
- joined_line_prefixes = " #{line_prefixes.join} "
+ changed_line_pairs.each do |old_index, new_index|
+ old_line = lines[old_index]
+ new_line = lines[new_index]
- offset = 0
- local_edit_indexes = []
- while index = joined_line_prefixes.index(" -+ ", offset)
- local_edit_indexes << index
- offset = index + 1
+ old_diffs, new_diffs = new(old_line, new_line, offset: 1).inline_diffs
+
+ inline_diffs[old_index] = old_diffs
+ inline_diffs[new_index] = new_diffs
+ end
+
+ inline_diffs
end
- local_edit_indexes
+ private
+
+ # Finds pairs of old/new line pairs that represent the same line that changed
+ def find_changed_line_pairs(lines)
+ # Prefixes of all diff lines, indicating their types
+ # For example: `" - + -+ ---+++ --+ -++"`
+ line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ')
+
+ changed_line_pairs = []
+ line_prefixes.scan(LINE_PAIRS_PATTERN) do
+ # For `"---+++"`, `begin_index == 0`, `end_index == 6`
+ begin_index, end_index = Regexp.last_match.offset(:del_ins)
+
+ # For `"---+++"`, `changed_line_count == 3`
+ changed_line_count = (end_index - begin_index) / 2
+
+ halfway_index = begin_index + changed_line_count
+ (begin_index...halfway_index).each do |i|
+ # For `"---+++"`, index 1 maps to 1 + 3 = 4
+ changed_line_pairs << [i, i + changed_line_count]
+ end
+ end
+
+ changed_line_pairs
+ end
end
+ private
+
def longest_common_prefix(a, b)
max_length = [a.length, b.length].max
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index c6189d660c2..80a146b4a5a 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -2,11 +2,27 @@ module Gitlab
module Diff
class Line
attr_reader :type, :index, :old_pos, :new_pos
+ attr_writer :rich_text
attr_accessor :text
- def initialize(text, type, index, old_pos, new_pos)
+ def initialize(text, type, index, old_pos, new_pos, parent_file: nil)
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
+ @parent_file = parent_file
+ end
+
+ def self.init_from_hash(hash)
+ new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
+ end
+
+ def serialize_keys
+ @serialize_keys ||= %i(text type index old_pos new_pos)
+ end
+
+ def to_hash
+ hash = {}
+ serialize_keys.each { |key| hash[key] = send(key) }
+ hash
end
def old_line
@@ -29,9 +45,25 @@ module Gitlab
type == 'old'
end
+ def rich_text
+ @parent_file.highlight_lines! if @parent_file && !@rich_text
+
+ @rich_text
+ end
+
def meta?
type == 'match' || type == 'nonewline'
end
+
+ def as_json(opts = nil)
+ {
+ type: type,
+ old_line: old_line,
+ new_line: new_line,
+ text: text,
+ rich_text: rich_text || text
+ }
+ end
end
end
end
diff --git a/lib/gitlab/diff/parallel_diff.rb b/lib/gitlab/diff/parallel_diff.rb
index 1c1fc148123..481536a380b 100644
--- a/lib/gitlab/diff/parallel_diff.rb
+++ b/lib/gitlab/diff/parallel_diff.rb
@@ -8,114 +8,52 @@ module Gitlab
end
def parallelize
- lines = []
- skip_next = false
+ i = 0
+ free_right_index = nil
+ lines = []
highlighted_diff_lines = diff_file.highlighted_diff_lines
highlighted_diff_lines.each do |line|
- full_line = line.text
- type = line.type
- line_code = diff_file.line_code(line)
- line_new = line.new_pos
- line_old = line.old_pos
- position = diff_file.position(line)
-
- next_line = diff_file.next_line(line.index)
-
- if next_line
- next_line = highlighted_diff_lines[next_line.index]
- full_next_line = next_line.text
- next_line_code = diff_file.line_code(next_line)
- next_type = next_line.type
- next_position = diff_file.position(next_line)
- end
-
- case type
- when 'match', nil
+ if line.meta? || line.unchanged?
# line in the right panel is the same as in the left one
lines << {
- left: {
- type: type,
- number: line_old,
- text: full_line,
- line_code: line_code,
- position: position
- },
- right: {
- type: type,
- number: line_new,
- text: full_line,
- line_code: line_code,
- position: position
- }
+ left: line,
+ right: line
}
- when 'old'
- case next_type
- when 'new'
- # Left side has text removed, right side has text added
- lines << {
- left: {
- type: type,
- number: line_old,
- text: full_line,
- line_code: line_code,
- position: position
- },
- right: {
- type: next_type,
- number: line_new,
- text: full_next_line,
- line_code: next_line_code,
- position: next_position,
- }
- }
- skip_next = true
- when 'old', 'nonewline', nil
- # Left side has text removed, right side doesn't have any change
- # No next line code, no new line number, no new line text
- lines << {
- left: {
- type: type,
- number: line_old,
- text: full_line,
- line_code: line_code,
- position: position
- },
- right: {
- type: next_type,
- number: nil,
- text: "",
- line_code: nil,
- position: nil
- }
- }
- end
- when 'new'
- if skip_next
- # Change has been already included in previous line so no need to do it again
- skip_next = false
- next
+
+ free_right_index = nil
+ i += 1
+ elsif line.removed?
+ lines << {
+ left: line,
+ right: nil
+ }
+
+ # Once we come upon a new line it can be put on the right of this old line
+ free_right_index ||= i
+ i += 1
+ elsif line.added?
+ if free_right_index
+ # If an old line came before this without a line on the right, this
+ # line can be put to the right of it.
+ lines[free_right_index][:right] = line
+
+ # If there are any other old lines on the left that don't yet have
+ # a new counterpart on the right, update the free_right_index
+ next_free_right_index = free_right_index + 1
+ free_right_index = next_free_right_index < i ? next_free_right_index : nil
else
- # Change is only on the right side, left side has no change
lines << {
- left: {
- type: nil,
- number: nil,
- text: "",
- line_code: line_code,
- position: position
- },
- right: {
- type: type,
- number: line_new,
- text: full_line,
- line_code: line_code,
- position: position
- }
+ left: nil,
+ right: line
}
+
+ free_right_index = nil
+ i += 1
end
end
end
+
lines
end
end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 989fff8918e..ecf62dead35 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -73,8 +73,8 @@ module Gitlab
diff_refs.complete?
end
- def to_json
- JSON.generate(self.to_h)
+ def to_json(opts = nil)
+ JSON.generate(self.to_h, opts)
end
def type
@@ -139,13 +139,19 @@ module Gitlab
private
def find_diff_file(repository)
- diffs = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- ).diffs(paths: paths)
+ # We're at the initial commit, so just get that as we can't compare to anything.
+ if Gitlab::Git.blank_ref?(start_sha)
+ compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ compare = Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
+
+ diff = compare.diffs(paths: paths).first
- diff = diffs.first
return unless diff
Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
diff --git a/lib/gitlab/downtime_check.rb b/lib/gitlab/downtime_check.rb
new file mode 100644
index 00000000000..ab9537ed7d7
--- /dev/null
+++ b/lib/gitlab/downtime_check.rb
@@ -0,0 +1,71 @@
+module Gitlab
+ # Checks if a set of migrations requires downtime or not.
+ class DowntimeCheck
+ # The constant containing the boolean that indicates if downtime is needed
+ # or not.
+ DOWNTIME_CONST = :DOWNTIME
+
+ # The constant that specifies the reason for the migration requiring
+ # downtime.
+ DOWNTIME_REASON_CONST = :DOWNTIME_REASON
+
+ # Checks the given migration paths and returns an Array of
+ # `Gitlab::DowntimeCheck::Message` instances.
+ #
+ # migrations - The migration file paths to check.
+ def check(migrations)
+ migrations.map do |path|
+ require(path)
+
+ migration_class = class_for_migration_file(path)
+
+ unless migration_class.const_defined?(DOWNTIME_CONST)
+ raise "The migration in #{path} does not specify if it requires " \
+ "downtime or not"
+ end
+
+ if online?(migration_class)
+ Message.new(path)
+ else
+ reason = downtime_reason(migration_class)
+
+ unless reason
+ raise "The migration in #{path} requires downtime but no reason " \
+ "was given"
+ end
+
+ Message.new(path, true, reason)
+ end
+ end
+ end
+
+ # Checks the given migrations and prints the results to STDOUT/STDERR.
+ #
+ # migrations - The migration file paths to check.
+ def check_and_print(migrations)
+ check(migrations).each do |message|
+ puts message.to_s # rubocop: disable Rails/Output
+ end
+ end
+
+ # Returns the class for the given migration file path.
+ def class_for_migration_file(path)
+ File.basename(path, File.extname(path)).split('_', 2).last.camelize.
+ constantize
+ end
+
+ # Returns true if the given migration can be performed without downtime.
+ def online?(migration)
+ migration.const_get(DOWNTIME_CONST) == false
+ end
+
+ # Returns the downtime reason, or nil if none was defined.
+ def downtime_reason(migration)
+ if migration.const_defined?(DOWNTIME_REASON_CONST)
+ migration.const_get(DOWNTIME_REASON_CONST)
+ else
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/downtime_check/message.rb b/lib/gitlab/downtime_check/message.rb
new file mode 100644
index 00000000000..40a4815a9a0
--- /dev/null
+++ b/lib/gitlab/downtime_check/message.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ class DowntimeCheck
+ class Message
+ attr_reader :path, :offline
+
+ OFFLINE = "\e[31moffline\e[0m"
+ ONLINE = "\e[32monline\e[0m"
+
+ # path - The file path of the migration.
+ # offline - When set to `true` the migration will require downtime.
+ # reason - The reason as to why the migration requires downtime.
+ def initialize(path, offline = false, reason = nil)
+ @path = path
+ @offline = offline
+ @reason = reason
+ end
+
+ def to_s
+ label = offline ? OFFLINE : ONLINE
+
+ message = "[#{label}]: #{path}"
+
+ if reason?
+ message += ":\n\n#{reason}\n\n"
+ end
+
+ message
+ end
+
+ def reason?
+ @reason.present?
+ end
+
+ def reason
+ @reason.strip.lines.map(&:strip).join("\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
new file mode 100644
index 00000000000..5cf9d5ebe28
--- /dev/null
+++ b/lib/gitlab/email/handler.rb
@@ -0,0 +1,18 @@
+require 'gitlab/email/handler/create_note_handler'
+require 'gitlab/email/handler/create_issue_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ # The `CreateIssueHandler` feature is disabled for the time being.
+ HANDLERS = [CreateNoteHandler]
+
+ def self.for(mail, mail_key)
+ HANDLERS.find do |klass|
+ handler = klass.new(mail, mail_key)
+ break handler if handler.can_handle?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/base_handler.rb b/lib/gitlab/email/handler/base_handler.rb
new file mode 100644
index 00000000000..7cccf465334
--- /dev/null
+++ b/lib/gitlab/email/handler/base_handler.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Email
+ module Handler
+ class BaseHandler
+ attr_reader :mail, :mail_key
+
+ def initialize(mail, mail_key)
+ @mail = mail
+ @mail_key = mail_key
+ end
+
+ def message
+ @message ||= process_message
+ end
+
+ def author
+ raise NotImplementedError
+ end
+
+ def project
+ raise NotImplementedError
+ end
+
+ private
+
+ def validate_permission!(permission)
+ raise UserNotFoundError unless author
+ raise UserBlockedError if author.blocked?
+ raise ProjectNotFound unless author.can?(:read_project, project)
+ raise UserNotAuthorizedError unless author.can?(permission, project)
+ end
+
+ def process_message
+ message = ReplyParser.new(mail).execute.strip
+ add_attachments(message)
+ end
+
+ def add_attachments(reply)
+ attachments = Email::AttachmentUploader.new(mail).execute(project)
+
+ reply + attachments.map do |link|
+ "\n\n#{link[:markdown]}"
+ end.join
+ end
+
+ def verify_record!(record:, invalid_exception:, record_name:)
+ return if record.persisted?
+ return if record.errors.key?(:commands_only)
+
+ error_title = "The #{record_name} could not be created for the following reasons:"
+
+ msg = error_title + record.errors.full_messages.map do |error|
+ "\n\n- #{error}"
+ end.join
+
+ raise invalid_exception, msg
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb
new file mode 100644
index 00000000000..4e6566af8ab
--- /dev/null
+++ b/lib/gitlab/email/handler/create_issue_handler.rb
@@ -0,0 +1,52 @@
+
+require 'gitlab/email/handler/base_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ class CreateIssueHandler < BaseHandler
+ attr_reader :project_path, :authentication_token
+
+ def initialize(mail, mail_key)
+ super(mail, mail_key)
+ @project_path, @authentication_token =
+ mail_key && mail_key.split('+', 2)
+ end
+
+ def can_handle?
+ !authentication_token.nil?
+ end
+
+ def execute
+ raise ProjectNotFound unless project
+
+ validate_permission!(:create_issue)
+
+ verify_record!(
+ record: create_issue,
+ invalid_exception: InvalidIssueError,
+ record_name: 'issue')
+ end
+
+ def author
+ @author ||= User.find_by(authentication_token: authentication_token)
+ end
+
+ def project
+ @project ||= Project.find_with_namespace(project_path)
+ end
+
+ private
+
+ def create_issue
+ Issues::CreateService.new(
+ project,
+ author,
+ title: mail.subject,
+ description: message
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/handler/create_note_handler.rb b/lib/gitlab/email/handler/create_note_handler.rb
new file mode 100644
index 00000000000..06dae31cc27
--- /dev/null
+++ b/lib/gitlab/email/handler/create_note_handler.rb
@@ -0,0 +1,55 @@
+
+require 'gitlab/email/handler/base_handler'
+
+module Gitlab
+ module Email
+ module Handler
+ class CreateNoteHandler < BaseHandler
+ def can_handle?
+ mail_key =~ /\A\w+\z/
+ end
+
+ def execute
+ raise SentNotificationNotFoundError unless sent_notification
+ raise AutoGeneratedEmailError if mail.header.to_s =~ /auto-(generated|replied)/
+
+ validate_permission!(:create_note)
+
+ raise NoteableNotFoundError unless sent_notification.noteable
+ raise EmptyEmailError if message.blank?
+
+ verify_record!(
+ record: create_note,
+ invalid_exception: InvalidNoteError,
+ record_name: 'comment')
+ end
+
+ def author
+ sent_notification.recipient
+ end
+
+ def project
+ sent_notification.project
+ end
+
+ def sent_notification
+ @sent_notification ||= SentNotification.for(mail_key)
+ end
+
+ private
+
+ def create_note
+ Notes::CreateService.new(
+ project,
+ author,
+ note: message,
+ noteable_type: sent_notification.noteable_type,
+ noteable_id: sent_notification.noteable_id,
+ commit_id: sent_notification.commit_id,
+ line_code: sent_notification.line_code
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 97701b0cd42..0e3b65fceb4 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -35,21 +35,22 @@ module Gitlab
def commits
return unless compare
- @commits ||= Commit.decorate(compare.commits, project)
+ @commits ||= compare.commits
end
def diffs
return unless compare
-
- @diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository)
+
+ # This diff is more moderated in number of files and lines
+ @diffs ||= compare.diffs(max_files: 30, max_lines: 5000, no_collapse: true).diff_files
end
def diffs_count
- diffs.count if diffs
+ diffs.size if diffs
end
def compare
- @opts[:compare]
+ @opts[:compare] if @opts[:compare]
end
def diff_refs
@@ -97,16 +98,18 @@ module Gitlab
if commits.length > 1
namespace_project_compare_url(project_namespace,
project,
- from: Commit.new(compare.base, project),
- to: Commit.new(compare.head, project))
+ from: compare.start_commit,
+ to: compare.head_commit)
else
namespace_project_commit_url(project_namespace,
- project, commits.first)
+ project,
+ commits.first)
end
else
unless @action == :delete
namespace_project_tree_url(project_namespace,
- project, ref_name)
+ project,
+ ref_name)
end
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 1c671a7487b..a40c44eb1bc 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -1,18 +1,24 @@
+
+require_dependency 'gitlab/email/handler'
+
# Inspired in great part by Discourse's Email::Receiver
module Gitlab
module Email
- class Receiver
- class ProcessingError < StandardError; end
- class EmailUnparsableError < ProcessingError; end
- class SentNotificationNotFoundError < ProcessingError; end
- class EmptyEmailError < ProcessingError; end
- class AutoGeneratedEmailError < ProcessingError; end
- class UserNotFoundError < ProcessingError; end
- class UserBlockedError < ProcessingError; end
- class UserNotAuthorizedError < ProcessingError; end
- class NoteableNotFoundError < ProcessingError; end
- class InvalidNoteError < ProcessingError; end
+ class ProcessingError < StandardError; end
+ class EmailUnparsableError < ProcessingError; end
+ class SentNotificationNotFoundError < ProcessingError; end
+ class ProjectNotFound < ProcessingError; end
+ class EmptyEmailError < ProcessingError; end
+ class AutoGeneratedEmailError < ProcessingError; end
+ class UserNotFoundError < ProcessingError; end
+ class UserBlockedError < ProcessingError; end
+ class UserNotAuthorizedError < ProcessingError; end
+ class NoteableNotFoundError < ProcessingError; end
+ class InvalidNoteError < ProcessingError; end
+ class InvalidIssueError < ProcessingError; end
+ class UnknownIncomingEmail < ProcessingError; end
+ class Receiver
def initialize(raw)
@raw = raw
end
@@ -20,91 +26,38 @@ module Gitlab
def execute
raise EmptyEmailError if @raw.blank?
- raise SentNotificationNotFoundError unless sent_notification
-
- raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/
-
- author = sent_notification.recipient
-
- raise UserNotFoundError unless author
-
- raise UserBlockedError if author.blocked?
-
- project = sent_notification.project
-
- raise UserNotAuthorizedError unless project && author.can?(:create_note, project)
-
- raise NoteableNotFoundError unless sent_notification.noteable
-
- reply = ReplyParser.new(message).execute.strip
-
- raise EmptyEmailError if reply.blank?
-
- reply = add_attachments(reply)
-
- note = create_note(reply)
+ mail = build_mail
+ mail_key = extract_mail_key(mail)
+ handler = Handler.for(mail, mail_key)
- unless note.persisted?
- msg = "The comment could not be created for the following reasons:"
- note.errors.full_messages.each do |error|
- msg << "\n\n- #{error}"
- end
+ raise UnknownIncomingEmail unless handler
- raise InvalidNoteError, msg
- end
+ handler.execute
end
- private
-
- def message
- @message ||= Mail::Message.new(@raw)
- rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
+ def build_mail
+ Mail::Message.new(@raw)
+ rescue Encoding::UndefinedConversionError,
+ Encoding::InvalidByteSequenceError => e
raise EmailUnparsableError, e
end
- def reply_key
- key_from_to_header || key_from_additional_headers
+ def extract_mail_key(mail)
+ key_from_to_header(mail) || key_from_additional_headers(mail)
end
- def key_from_to_header
- key = nil
- message.to.each do |address|
+ def key_from_to_header(mail)
+ mail.to.find do |address|
key = Gitlab::IncomingEmail.key_from_address(address)
- break if key
+ break key if key
end
-
- key
end
- def key_from_additional_headers
- reply_key = nil
-
- Array(message.references).each do |message_id|
- reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
- break if reply_key
+ def key_from_additional_headers(mail)
+ Array(mail.references).find do |mail_id|
+ key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
+ break key if key
end
-
- reply_key
- end
-
- def sent_notification
- return nil unless reply_key
-
- SentNotification.for(reply_key)
- end
-
- def add_attachments(reply)
- attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project)
-
- attachments.each do |link|
- reply << "\n\n#{link[:markdown]}"
- end
-
- reply
- end
-
- def create_note(reply)
- sent_notification.create_note(reply)
end
end
end
diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb
deleted file mode 100644
index 93c6a5bb7f5..00000000000
--- a/lib/gitlab/force_push_check.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-module Gitlab
- class ForcePushCheck
- def self.force_push?(project, oldrev, newrev)
- return false if project.empty_repo?
-
- # Created or deleted branch
- if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
- false
- else
- missed_refs, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
- missed_refs.split("\n").size > 0
- end
- end
- end
-end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 191bea86ac3..7584efe4fa8 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -9,6 +9,24 @@ module Gitlab
ref.gsub(/\Arefs\/(tags|heads)\//, '')
end
+ def branch_name(ref)
+ ref = ref.to_s
+ if self.branch_ref?(ref)
+ self.ref_name(ref)
+ else
+ nil
+ end
+ end
+
+ def tag_name(ref)
+ ref = ref.to_s
+ if self.tag_ref?(ref)
+ self.ref_name(ref)
+ else
+ nil
+ end
+ end
+
def tag_ref?(ref)
ref.start_with?(TAG_REF_PREFIX)
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 7679c7e4bb8..1882eb8d050 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -1,66 +1,31 @@
+# Check a user's access to perform a git action. All public methods in this
+# class return an instance of `GitlabAccessStatus`
module Gitlab
class GitAccess
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
- attr_reader :actor, :project, :protocol
+ attr_reader :actor, :project, :protocol, :user_access
def initialize(actor, project, protocol)
@actor = actor
@project = project
@protocol = protocol
+ @user_access = UserAccess.new(user, project: project)
end
- def user
- return @user if defined?(@user)
-
- @user =
- case actor
- when User
- actor
- when DeployKey
- nil
- when Key
- actor.user
- end
- end
-
- def deploy_key
- actor if actor.is_a?(DeployKey)
- end
-
- def can_push_to_branch?(ref)
- return false unless user
-
- if project.protected_branch?(ref) && !project.developers_can_push_to_protected_branch?(ref)
- user.can?(:push_code_to_protected_branches, project)
- else
- user.can?(:push_code, project)
- end
- end
-
- def can_read_project?
- if user
- user.can?(:read_project, project)
- elsif deploy_key
- deploy_key.projects.include?(project)
- else
- false
- end
- end
-
- def check(cmd, changes = nil)
+ 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_allowed?
+ if user && !user_access.allowed?
return build_status_object(false, "Your account has been blocked.")
end
- unless project && can_read_project?
+ 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
@@ -95,7 +60,7 @@ module Gitlab
end
def user_download_access_check
- unless user.can?(:download_code, project)
+ unless user_access.can_do_action?(:download_code)
return build_status_object(false, "You are not allowed to download code from this project.")
end
@@ -111,10 +76,10 @@ module Gitlab
return build_status_object(false, "A repository for this project does not exist yet.")
end
- changes = changes.lines if changes.kind_of?(String)
+ changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied
- changes.map(&:strip).reject(&:blank?).each do |change|
+ changes_list.each do |change|
status = change_access_check(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
@@ -125,46 +90,8 @@ module Gitlab
build_status_object(true)
end
- def can_user_do_action?(action)
- @permission_cache ||= {}
- @permission_cache[action] ||= user.can?(action, project)
- end
-
def change_access_check(change)
- oldrev, newrev, ref = change.split(' ')
-
- action =
- if project.protected_branch?(branch_name(ref))
- protected_branch_action(oldrev, newrev, branch_name(ref))
- elsif (tag_ref = tag_name(ref)) && protected_tag?(tag_ref)
- # Prevent any changes to existing git tag unless user has permissions
- :admin_project
- else
- :push_code
- end
-
- unless can_user_do_action?(action)
- status =
- case action
- when :force_push_code_to_protected_branches
- build_status_object(false, "You are not allowed to force push code to a protected branch on this project.")
- when :remove_protected_branches
- build_status_object(false, "You are not allowed to deleted protected branches from this project.")
- when :push_code_to_protected_branches
- build_status_object(false, "You are not allowed to push code to protected branches on this project.")
- when :admin_project
- build_status_object(false, "You are not allowed to change existing tags on this project.")
- else # :push_code
- build_status_object(false, "You are not allowed to push code to this project.")
- end
- return status
- end
-
- build_status_object(true)
- end
-
- def forced_push?(oldrev, newrev)
- Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev)
+ Checks::ChangeAccess.new(change, user_access: user_access, project: project).exec
end
def protocol_allowed?
@@ -173,50 +100,41 @@ module Gitlab
private
- def protected_branch_action(oldrev, newrev, branch_name)
- # we dont allow force push to protected branch
- if forced_push?(oldrev, newrev)
- :force_push_code_to_protected_branches
- elsif Gitlab::Git.blank_ref?(newrev)
- # and we dont allow remove of protected branch
- :remove_protected_branches
- elsif project.developers_can_push_to_protected_branch?(branch_name)
- :push_code
- else
- :push_code_to_protected_branches
- end
+ def matching_merge_request?(newrev, branch_name)
+ Checks::MatchingMergeRequest.new(newrev, branch_name, project).match?
end
- def protected_tag?(tag_name)
- project.repository.tag_exists?(tag_name)
- end
-
- def user_allowed?
- Gitlab::UserAccess.allowed?(user)
- end
-
- def branch_name(ref)
- ref = ref.to_s
- if Gitlab::Git.branch_ref?(ref)
- Gitlab::Git.ref_name(ref)
- else
- nil
- end
+ def deploy_key
+ actor if actor.is_a?(DeployKey)
end
- def tag_name(ref)
- ref = ref.to_s
- if Gitlab::Git.tag_ref?(ref)
- Gitlab::Git.ref_name(ref)
+ def deploy_key_can_read_project?
+ if deploy_key
+ return true if project.public?
+ deploy_key.projects.include?(project)
else
- nil
+ false
end
end
protected
+ def user
+ return @user if defined?(@user)
+
+ @user =
+ case actor
+ when User
+ actor
+ when DeployKey
+ nil
+ when Key
+ actor.user
+ end
+ end
+
def build_status_object(status, message = '')
- GitAccessStatus.new(status, message)
+ Gitlab::GitAccessStatus.new(status, message)
end
end
end
diff --git a/lib/gitlab/git_access_status.rb b/lib/gitlab/git_access_status.rb
index 5a806ff6e0d..09bb01be694 100644
--- a/lib/gitlab/git_access_status.rb
+++ b/lib/gitlab/git_access_status.rb
@@ -8,8 +8,8 @@ module Gitlab
@message = message
end
- def to_json
- { status: @status, message: @message }.to_json
+ def to_json(opts = nil)
+ { status: @status, message: @message }.to_json(opts)
end
end
end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 8672cbc0ec4..f71d3575909 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,7 +1,7 @@
module Gitlab
class GitAccessWiki < GitAccess
def change_access_check(change)
- if user.can?(:create_wiki, project)
+ if user_access.can_do_action?(:create_wiki)
build_status_object(true)
else
build_status_object(false, "You are not allowed to write to this project's wiki.")
diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb
index a088e19d1e7..d32bdd86427 100644
--- a/lib/gitlab/git_post_receive.rb
+++ b/lib/gitlab/git_post_receive.rb
@@ -39,7 +39,6 @@ module Gitlab
end
def deserialize_changes(changes)
- changes = Base64.decode64(changes) unless changes.include?(' ')
changes = utf8_encode_changes(changes)
changes.lines
end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 7d2d545b84e..4750675ae9d 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -7,10 +7,6 @@ module Gitlab
branch_exists? && commit_exists?
end
- def name
- @name ||= exists? ? ref : "#{ref}-#{short_id}"
- end
-
def valid?
repo.present?
end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 043f10d96a9..084e514492c 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -78,10 +78,21 @@ module Gitlab
def rate_limit
api.rate_limit!
+ # GitHub Rate Limit API returns 404 when the rate limit is
+ # disabled. In this case we just want to return gracefully
+ # instead of spitting out an error.
+ rescue Octokit::NotFound
+ nil
+ end
+
+ def has_rate_limit?
+ return @has_rate_limit if defined?(@has_rate_limit)
+
+ @has_rate_limit = rate_limit.present?
end
def rate_limit_exceed?
- rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
+ has_rate_limit? && rate_limit.remaining <= GITHUB_SAFE_REMAINING_REQUESTS
end
def rate_limit_sleep_time
diff --git a/lib/gitlab/github_import/hook_formatter.rb b/lib/gitlab/github_import/hook_formatter.rb
deleted file mode 100644
index db1fabaa18a..00000000000
--- a/lib/gitlab/github_import/hook_formatter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
- module GithubImport
- class HookFormatter
- EVENTS = %w[* create delete pull_request push].freeze
-
- attr_reader :raw
-
- delegate :id, :name, :active, to: :raw
-
- def initialize(raw)
- @raw = raw
- end
-
- def config
- raw.config.attrs
- end
-
- def valid?
- (EVENTS & raw.events).any? && active
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 3932fcb1eda..02ffb43d89b 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,24 +3,30 @@ module Gitlab
class Importer
include Gitlab::ShellAdapter
- attr_reader :client, :project, :repo, :repo_url
+ attr_reader :client, :errors, :project, :repo, :repo_url
def initialize(project)
@project = project
@repo = project.import_source
@repo_url = project.import_url
+ @errors = []
if credentials
@client = Client.new(credentials[:user])
- @formatter = Gitlab::ImportFormatter.new
else
raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{@project.id}"
end
end
def execute
- import_labels && import_milestones && import_issues &&
- import_pull_requests && import_wiki
+ import_labels
+ import_milestones
+ import_issues
+ import_pull_requests
+ import_wiki
+ handle_errors
+
+ true
end
private
@@ -29,22 +35,37 @@ module Gitlab
@credentials ||= project.import_data.credentials if project.import_data
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 import_labels
labels = client.labels(repo, per_page: 100)
- labels.each { |raw| LabelFormatter.new(project, raw).create! }
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ labels.each do |raw|
+ begin
+ LabelFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
end
def import_milestones
milestones = client.milestones(repo, state: :all, per_page: 100)
- milestones.each { |raw| MilestoneFormatter.new(project, raw).create! }
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ milestones.each do |raw|
+ begin
+ MilestoneFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
end
def import_issues
@@ -54,85 +75,55 @@ module Gitlab
gh_issue = IssueFormatter.new(project, raw)
if gh_issue.valid?
- issue = gh_issue.create!
- apply_labels(issue)
- import_comments(issue) if gh_issue.has_comments?
+ begin
+ issue = gh_issue.create!
+ apply_labels(issue)
+ import_comments(issue) if gh_issue.has_comments?
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
-
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
end
def import_pull_requests
- disable_webhooks
-
pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
- source_branches_removed = pull_requests.reject(&:source_branch_exists?).map { |pr| [pr.source_branch_name, pr.source_branch_sha] }
- target_branches_removed = pull_requests.reject(&:target_branch_exists?).map { |pr| [pr.target_branch_name, pr.target_branch_sha] }
- branches_removed = source_branches_removed | target_branches_removed
-
- restore_branches(branches_removed)
-
pull_requests.each do |pull_request|
- merge_request = pull_request.create!
- apply_labels(merge_request)
- import_comments(merge_request)
- import_comments_on_diff(merge_request)
+ begin
+ 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)
+ import_comments(merge_request)
+ import_comments_on_diff(merge_request)
+ rescue => e
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+ ensure
+ clean_up_restored_branches(pull_request)
+ end
end
-
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
- ensure
- clean_up_restored_branches(branches_removed)
- clean_up_disabled_webhooks
- end
-
- def disable_webhooks
- update_webhooks(hooks, active: false)
end
- def clean_up_disabled_webhooks
- update_webhooks(hooks, active: true)
+ def restore_source_branch(pull_request)
+ project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name)
end
- def update_webhooks(hooks, options)
- hooks.each do |hook|
- client.edit_hook(repo, hook.id, hook.name, hook.config, options)
- end
+ def restore_target_branch(pull_request)
+ project.repository.create_branch(pull_request.target_branch_name, pull_request.target_branch_sha)
end
- def hooks
- @hooks ||=
- begin
- client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
-
- # The GitHub Repository Webhooks API returns 404 for users
- # without admin access to the repository when listing hooks.
- # In this case we just want to return gracefully instead of
- # spitting out an error and stop the import process.
- rescue Octokit::NotFound
- []
- end
+ def remove_branch(name)
+ project.repository.delete_branch(name)
+ rescue Rugged::ReferenceError
+ errors << { type: :remove_branch, name: name }
end
- def restore_branches(branches)
- branches.each do |name, sha|
- client.create_ref(repo, "refs/heads/#{name}", sha)
- end
-
- project.repository.fetch_ref(repo_url, '+refs/heads/*', 'refs/heads/*')
- end
-
- def clean_up_restored_branches(branches)
- branches.each do |name, _|
- client.delete_ref(repo, "heads/#{name}")
- project.repository.delete_branch(name) rescue Rugged::ReferenceError
- end
+ def clean_up_restored_branches(pull_request)
+ remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
+ remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
project.repository.after_remove_branch
end
@@ -141,9 +132,10 @@ module Gitlab
issue = client.issue(repo, issuable.iid)
if issue.labels.count > 0
- label_ids = issue.labels.map do |raw|
- Label.find_by(LabelFormatter.new(project, raw).attributes).try(:id)
- end
+ label_ids = issue.labels
+ .map { |raw| LabelFormatter.new(project, raw).attributes }
+ .map { |attrs| Label.find_by(attrs).try(:id) }
+ .compact
issuable.update_attribute(:label_ids, label_ids)
end
@@ -161,8 +153,12 @@ module Gitlab
def create_comments(issuable, comments)
comments.each do |raw|
- comment = CommentFormatter.new(project, raw)
- issuable.notes.create!(comment.attributes)
+ begin
+ comment = CommentFormatter.new(project, raw)
+ issuable.notes.create!(comment.attributes)
+ rescue => e
+ errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
@@ -172,16 +168,12 @@ module Gitlab
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.update_attribute(:wiki_enabled, true)
end
-
- true
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
# this means that repo has wiki enabled, but have no pages. So,
# we can skip the import.
if e.message !~ /repository not exported/
- raise Projects::ImportService::Error, e.message
- else
- true
+ errors << { type: :wiki, errors: e.message }
end
end
end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index a4ea2210abd..04aa3664f64 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,8 +1,8 @@
module Gitlab
module GithubImport
class PullRequestFormatter < BaseFormatter
- delegate :exists?, :name, :project, :repo, :sha, to: :source_branch, prefix: true
- delegate :exists?, :name, :project, :repo, :sha, to: :target_branch, prefix: true
+ delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
+ delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
def attributes
{
@@ -33,17 +33,33 @@ module Gitlab
end
def valid?
- source_branch.valid? && target_branch.valid? && !cross_project?
+ source_branch.valid? && target_branch.valid?
end
def source_branch
@source_branch ||= BranchFormatter.new(project, raw_data.head)
end
+ def source_branch_name
+ @source_branch_name ||= begin
+ source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ end
+ end
+
def target_branch
@target_branch ||= BranchFormatter.new(project, raw_data.base)
end
+ def target_branch_name
+ @target_branch_name ||= begin
+ target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}"
+ end
+ end
+
+ def url
+ raw_data.url
+ end
+
private
def assigned?
@@ -68,10 +84,6 @@ module Gitlab
raw_data.body || ""
end
- def cross_project?
- source_branch_repo.id != target_branch_repo.id
- end
-
def description
formatter.author_line(author) + body
end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 3f76ec97977..46d40f75be6 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -15,31 +15,35 @@ module Gitlab
end
def execute
- project_identifier = CGI.escape(project.import_source)
-
- # Issues && Comments
- issues = client.issues(project_identifier)
-
- issues.each do |issue|
- body = @formatter.author_line(issue["author"]["name"])
- body += issue["description"]
-
- comments = client.issue_comments(project_identifier, issue["id"])
-
- if comments.any?
- body += @formatter.comments_header
+ ActiveRecord::Base.no_touching do
+ project_identifier = CGI.escape(project.import_source)
+
+ # Issues && Comments
+ issues = client.issues(project_identifier)
+
+ issues.each do |issue|
+ body = @formatter.author_line(issue["author"]["name"])
+ body += issue["description"]
+
+ comments = client.issue_comments(project_identifier, issue["id"])
+
+ if comments.any?
+ body += @formatter.comments_header
+ end
+
+ comments.each do |comment|
+ body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
+ end
+
+ project.issues.create!(
+ iid: issue["iid"],
+ description: body,
+ title: issue["title"],
+ state: issue["state"],
+ updated_at: issue["updated_at"],
+ author_id: gl_user_id(project, issue["author"]["id"])
+ )
end
-
- comments.each do |comment|
- body += @formatter.comment(comment["author"]["name"], comment["created_at"], comment["body"])
- end
-
- project.issues.create!(
- description: body,
- title: issue["title"],
- state: issue["state"],
- author_id: gl_user_id(project, issue["author"]["id"])
- )
end
true
diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb
deleted file mode 100644
index 8d0132a744c..00000000000
--- a/lib/gitlab/gitorious_import.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Gitlab
- module GitoriousImport
- GITORIOUS_HOST = "https://gitorious.org"
- end
-end
diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb
deleted file mode 100644
index 99fe5bdebfc..00000000000
--- a/lib/gitlab/gitorious_import/client.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Gitlab
- module GitoriousImport
- class Client
- attr_reader :repo_list
-
- def initialize(repo_list)
- @repo_list = repo_list
- end
-
- def authorize_url(redirect_uri)
- "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}"
- end
-
- def repos
- @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
- end
-
- def repo(id)
- repos.find { |repo| repo.id == id }
- end
-
- private
-
- def repo_names
- repo_list.to_s.split(',').map(&:strip).reject(&:blank?)
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
deleted file mode 100644
index 8e22aa9286d..00000000000
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module GitoriousImport
- class ProjectCreator
- attr_reader :repo, :namespace, :current_user
-
- def initialize(repo, namespace, current_user)
- @repo = repo
- @namespace = namespace
- @current_user = current_user
- end
-
- def execute
- ::Projects::CreateService.new(
- current_user,
- name: repo.name,
- path: repo.path,
- description: repo.description,
- namespace_id: namespace.id,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC,
- import_type: "gitorious",
- import_source: repo.full_name,
- import_url: repo.import_url
- ).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb
deleted file mode 100644
index c88f1ae358d..00000000000
--- a/lib/gitlab/gitorious_import/repository.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Gitlab
- module GitoriousImport
- Repository = Struct.new(:full_name) do
- def id
- Digest::SHA1.hexdigest(full_name)
- end
-
- def namespace
- segments.first
- end
-
- def path
- segments.last
- end
-
- def name
- path.titleize
- end
-
- def description
- ""
- end
-
- def import_url
- "#{GITORIOUS_HOST}/#{full_name}.git"
- end
-
- private
-
- def segments
- full_name.split('/')
- end
- end
- end
-end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index d4f12cb1df9..c5a11148d33 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -5,7 +5,7 @@ module Gitlab
gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
gon.max_file_size = current_application_settings.max_attachment_size
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
- gon.shortcuts_path = help_shortcuts_path
+ 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
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 41296415e35..9360afedfcb 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -1,7 +1,7 @@
module Gitlab
class Highlight
- def self.highlight(blob_name, blob_content, repository: nil, nowrap: true, plain: false)
- new(blob_name, blob_content, nowrap: nowrap, repository: repository).
+ def self.highlight(blob_name, blob_content, repository: nil, plain: false)
+ new(blob_name, blob_content, repository: repository).
highlight(blob_content, continue: false, plain: plain)
end
@@ -13,30 +13,34 @@ module Gitlab
highlight(file_name, blob.data, repository: repository).lines.map!(&:html_safe)
end
- attr_reader :lexer
- def initialize(blob_name, blob_content, repository: nil, nowrap: true)
+ def initialize(blob_name, blob_content, repository: nil)
+ @formatter = Rouge::Formatters::HTMLGitlab.new
+ @repository = repository
@blob_name = blob_name
@blob_content = blob_content
- @repository = repository
- @formatter = rouge_formatter(nowrap: nowrap)
-
- @lexer = custom_language || begin
- Rouge::Lexer.guess(filename: blob_name, source: blob_content).new
- rescue Rouge::Lexer::AmbiguousGuess => e
- e.alternatives.sort_by(&:tag).first
- end
end
def highlight(text, continue: true, plain: false)
if plain
- @formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
+ hl_lexer = Rouge::Lexers::PlainText
+ continue = false
else
- @formatter.format(@lexer.lex(text, continue: continue)).html_safe
+ hl_lexer = self.lexer
end
+
+ @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe
rescue
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end
+ def lexer
+ @lexer ||= custom_language || begin
+ Rouge::Lexer.guess(filename: @blob_name, source: @blob_content).new
+ rescue Rouge::Guesser::Ambiguous => e
+ e.alternatives.sort_by(&:tag).first
+ end
+ end
+
private
def custom_language
@@ -46,16 +50,5 @@ module Gitlab
Rouge::Lexer.find_fancy(language_name)
end
-
- def rouge_formatter(options = {})
- options = options.reverse_merge(
- nowrap: true,
- cssclass: 'code highlight',
- lineanchors: true,
- lineanchorsid: 'LC'
- )
-
- Rouge::Formatters::HTMLGitlab.new(options)
- end
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 588647e5adb..bb562bdcd2c 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -2,7 +2,8 @@ module Gitlab
module ImportExport
extend self
- VERSION = '0.1.1'
+ VERSION = '0.1.3'
+ FILENAME_LIMIT = 50
def export_path(relative_path:)
File.join(storage_path, relative_path)
@@ -12,6 +13,10 @@ module Gitlab
File.join(Settings.shared['path'], 'tmp/project_exports')
end
+ def import_upload_path(filename:)
+ File.join(storage_path, 'uploads', filename)
+ end
+
def project_filename
"project.json"
end
@@ -28,6 +33,12 @@ module Gitlab
'VERSION'
end
+ def export_filename(project:)
+ basename = "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_#{project.namespace.path}_#{project.path}"
+
+ "#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
+ end
+
def version
VERSION
end
diff --git a/lib/gitlab/import_export/avatar_restorer.rb b/lib/gitlab/import_export/avatar_restorer.rb
new file mode 100644
index 00000000000..cfa595629f4
--- /dev/null
+++ b/lib/gitlab/import_export/avatar_restorer.rb
@@ -0,0 +1,30 @@
+module Gitlab
+ module ImportExport
+ class AvatarRestorer
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def restore
+ return true unless avatar_export_file
+
+ @project.avatar = File.open(avatar_export_file)
+ @project.save!
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def avatar_export_file
+ @avatar_export_file ||= Dir["#{avatar_export_path}/*"].first
+ end
+
+ def avatar_export_path
+ File.join(@shared.export_path, 'avatar')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/avatar_saver.rb b/lib/gitlab/import_export/avatar_saver.rb
new file mode 100644
index 00000000000..998c21e2586
--- /dev/null
+++ b/lib/gitlab/import_export/avatar_saver.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module ImportExport
+ class AvatarSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(project:, shared:)
+ @project = project
+ @shared = shared
+ end
+
+ def save
+ return true unless @project.avatar.exists?
+
+ copy_files(avatar_path, avatar_export_path)
+ rescue => e
+ @shared.error(e)
+ false
+ end
+
+ private
+
+ def avatar_export_path
+ File.join(@shared.export_path, 'avatar', @project.avatar_identifier)
+ end
+
+ def avatar_path
+ @project.avatar.path
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index 2249904145c..e522a0fc8f6 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -17,6 +17,10 @@ module Gitlab
execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
end
+ def git_restore_hooks
+ execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
+ end
+
private
def tar_with_options(archive:, dir:, options:)
@@ -36,6 +40,19 @@ module Gitlab
def git_bin_path
Gitlab.config.git.bin_path
end
+
+ def copy_files(source, destination)
+ # if we are copying files, create the destination folder
+ destination_folder = File.file?(source) ? File.dirname(destination) : destination
+
+ FileUtils.mkdir_p(destination_folder)
+ FileUtils.copy_entry(source, destination)
+ true
+ end
+
+ def repository_storage_paths_args
+ Gitlab.config.repositories.storages.values
+ end
end
end
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index 82d1e1805c5..eca6e5b6d51 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -3,6 +3,8 @@ module Gitlab
class FileImporter
include Gitlab::ImportExport::CommandLineUtil
+ MAX_RETRIES = 8
+
def self.import(*args)
new(*args).import
end
@@ -14,7 +16,10 @@ module Gitlab
def import
FileUtils.mkdir_p(@shared.export_path)
- decompress_archive
+
+ wait_for_archived_file do
+ decompress_archive
+ end
rescue => e
@shared.error(e)
false
@@ -22,6 +27,17 @@ module Gitlab
private
+ # Exponentially sleep until I/O finishes copying the file
+ def wait_for_archived_file
+ MAX_RETRIES.times do |retry_number|
+ break if File.exist?(@archive_file)
+
+ sleep(2**retry_number)
+ end
+
+ yield
+ end
+
def decompress_archive
result = untar_zxf(archive: @archive_file, dir: @shared.export_path)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 05f4ad527ac..1da51043611 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -3,11 +3,12 @@ project_tree:
- issues:
- :events
- notes:
- - :author
- - :events
- - :labels
- - milestones:
- - :events
+ - :author
+ - :events
+ - label_links:
+ - :label
+ - milestone:
+ - :events
- snippets:
- notes:
:author
@@ -20,6 +21,10 @@ project_tree:
- :events
- :merge_request_diff
- :events
+ - label_links:
+ - :label
+ - milestone:
+ - :events
- pipelines:
- notes:
- :author
@@ -31,6 +36,9 @@ project_tree:
- :services
- :hooks
- :protected_branches
+ - :labels
+ - milestones:
+ - :events
# Only include the following attributes for the models specified.
included_attributes:
@@ -53,7 +61,15 @@ included_attributes:
excluded_attributes:
snippets:
- :expired_at
+ merge_request_diff:
+ - :st_diffs
+ issues:
+ - :milestone_id
+ merge_requests:
+ - :milestone_id
methods:
statuses:
- - :type \ No newline at end of file
+ - :type
+ merge_request_diff:
+ - :utf8_st_diffs \ No newline at end of file
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 8f66f48cbfe..e9ee47fc090 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def execute
- if import_file && check_version! && [project_tree, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
+ if import_file && check_version! && [project_tree, avatar_restorer, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
@@ -35,6 +35,10 @@ module Gitlab
project: @project)
end
+ def avatar_restorer
+ Gitlab::ImportExport::AvatarRestorer.new(project: project_tree.restored_project, shared: @shared)
+ end
+
def repo_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: repo_path,
shared: @shared,
@@ -44,8 +48,7 @@ module Gitlab
def wiki_restorer
Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: wiki_repo_path,
shared: @shared,
- project: ProjectWiki.new(project_tree.restored_project),
- wiki: true)
+ project: ProjectWiki.new(project_tree.restored_project))
end
def uploads_restorer
diff --git a/lib/gitlab/import_export/json_hash_builder.rb b/lib/gitlab/import_export/json_hash_builder.rb
new file mode 100644
index 00000000000..0cc10f40087
--- /dev/null
+++ b/lib/gitlab/import_export/json_hash_builder.rb
@@ -0,0 +1,107 @@
+module Gitlab
+ module ImportExport
+ # Generates a hash that conforms with http://apidock.com/rails/Hash/to_json
+ # and its peculiar options.
+ class JsonHashBuilder
+ def self.build(model_objects, attributes_finder)
+ new(model_objects, attributes_finder).build
+ end
+
+ def initialize(model_objects, attributes_finder)
+ @model_objects = model_objects
+ @attributes_finder = attributes_finder
+ end
+
+ def build
+ process_model_objects(@model_objects)
+ end
+
+ private
+
+ # Called when the model is actually a hash containing other relations (more models)
+ # Returns the config in the right format for calling +to_json+
+ #
+ # +model_object_hash+ - A model relationship such as:
+ # {:merge_requests=>[:merge_request_diff, :notes]}
+ def process_model_objects(model_object_hash)
+ json_config_hash = {}
+ current_key = model_object_hash.keys.first
+
+ model_object_hash.values.flatten.each do |model_object|
+ @attributes_finder.parse(current_key) { |hash| json_config_hash[current_key] ||= hash }
+ handle_model_object(current_key, model_object, json_config_hash)
+ end
+
+ json_config_hash
+ end
+
+ # Creates or adds to an existing hash an individual model or list
+ #
+ # +current_key+ main model that will be a key in the hash
+ # +model_object+ model or list of models to include in the hash
+ # +json_config_hash+ the original hash containing the root model
+ def handle_model_object(current_key, model_object, json_config_hash)
+ model_or_sub_model = model_object.is_a?(Hash) ? process_model_objects(model_object) : model_object
+
+ if json_config_hash[current_key]
+ add_model_value(current_key, model_or_sub_model, json_config_hash)
+ else
+ create_model_value(current_key, model_or_sub_model, json_config_hash)
+ end
+ end
+
+ # Constructs a new hash that will hold the configuration for that particular object
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ #
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ # +json_config_hash+ the original hash containing the root model
+ def create_model_value(current_key, value, json_config_hash)
+ json_config_hash[current_key] = parse_hash(value) || { include: value }
+ end
+
+ # Calls attributes finder to parse the hash and add any attributes to it
+ #
+ # +value+ existing model to be included in the hash
+ # +parsed_hash+ the original hash
+ def parse_hash(value)
+ @attributes_finder.parse(value) do |hash|
+ { include: hash_or_merge(value, hash) }
+ end
+ end
+
+ # Adds new model configuration to an existing hash with key +current_key+
+ # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
+ #
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ # +json_config_hash+ the original hash containing the root model
+ def add_model_value(current_key, value, json_config_hash)
+ @attributes_finder.parse(value) { |hash| value = { value => hash } }
+
+ add_to_array(current_key, json_config_hash, value)
+ end
+
+ # Adds new model configuration to an existing hash with key +current_key+
+ # it creates a new array if it was previously a single value
+ #
+ # +current_key+ main model that will be a key in the hash
+ # +value+ existing model to be included in the hash
+ # +json_config_hash+ the original hash containing the root model
+ def add_to_array(current_key, json_config_hash, value)
+ old_values = json_config_hash[current_key][:include]
+
+ json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
+ end
+
+ # Construct a new hash or merge with an existing one a model configuration
+ # This is to fulfil +to_json+ requirements.
+ #
+ # +hash+ hash containing configuration generated mainly from +@attributes_finder+
+ # +value+ existing model to be included in the hash
+ def hash_or_merge(value, hash)
+ value.is_a?(Hash) ? value.merge(hash) : { value => hash }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index b459054c198..36c4cf6efa0 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -18,11 +18,14 @@ module Gitlab
@map ||=
begin
@exported_members.inject(missing_keys_tracking_hash) do |hash, member|
- existing_user = User.where(find_project_user_query(member)).first
- old_user_id = member['user']['id']
- if existing_user && add_user_as_team_member(existing_user, member)
- hash[old_user_id] = existing_user.id
+ if member['user']
+ old_user_id = member['user']['id']
+ existing_user = User.where(find_project_user_query(member)).first
+ hash[old_user_id] = existing_user.id if existing_user && add_team_member(member, existing_user)
+ else
+ add_team_member(member)
end
+
hash
end
end
@@ -45,7 +48,7 @@ module Gitlab
ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true)
end
- def add_user_as_team_member(existing_user, member)
+ def add_team_member(member, existing_user = nil)
member['user'] = existing_user
ProjectMember.create(member_hash(member)).persisted?
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index 0ac6ff01e3b..c7b3551b84c 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -12,7 +12,10 @@ module Gitlab
json = IO.read(@path)
@tree_hash = ActiveSupport::JSON.decode(json)
@project_members = @tree_hash.delete('project_members')
- create_relations
+
+ ActiveRecord::Base.no_touching do
+ create_relations
+ end
rescue => e
@shared.error(e)
false
@@ -44,7 +47,7 @@ module Gitlab
relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s])
- saved << restored_project.update_attribute(relation_key, relation_hash)
+ saved << restored_project.append_or_update_attribute(relation_key, relation_hash)
end
saved.all?
end
@@ -69,10 +72,19 @@ module Gitlab
# Example:
# +relation_key+ issues, loops through the list of *issues* and for each individual
# issue, finds any subrelations such as notes, creates them and assign them back to the hash
+ #
+ # Recursively calls this method if the sub-relation is a hash containing more sub-relations
def create_sub_relations(relation, tree_hash)
relation_key = relation.keys.first.to_s
- tree_hash[relation_key].each do |relation_item|
+ return if tree_hash[relation_key].blank?
+
+ [tree_hash[relation_key]].flatten.each do |relation_item|
relation.values.flatten.each do |sub_relation|
+ # We just use author to get the user ID, do not attempt to create an instance.
+ next if sub_relation == :author
+
+ create_sub_relations(sub_relation, relation_item) if sub_relation.is_a?(Hash)
+
relation_hash, sub_relation = assign_relation_hash(relation_item, sub_relation)
relation_item[sub_relation.to_s] = create_relation(sub_relation, relation_hash) unless relation_hash.blank?
end
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 15f5dd31035..5021a1a14ce 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -29,87 +29,12 @@ module Gitlab
def build_hash(model_list)
model_list.map do |model_objects|
if model_objects.is_a?(Hash)
- build_json_config_hash(model_objects)
+ Gitlab::ImportExport::JsonHashBuilder.build(model_objects, @attributes_finder)
else
@attributes_finder.find(model_objects)
end
end
end
-
- # Called when the model is actually a hash containing other relations (more models)
- # Returns the config in the right format for calling +to_json+
- # +model_object_hash+ - A model relationship such as:
- # {:merge_requests=>[:merge_request_diff, :notes]}
- def build_json_config_hash(model_object_hash)
- @json_config_hash = {}
-
- model_object_hash.values.flatten.each do |model_object|
- current_key = model_object_hash.keys.first
-
- @attributes_finder.parse(current_key) { |hash| @json_config_hash[current_key] ||= hash }
-
- handle_model_object(current_key, model_object)
- process_sub_model(current_key, model_object) if model_object.is_a?(Hash)
- end
- @json_config_hash
- end
-
- # If the model is a hash, process the sub_models, which could also be hashes
- # If there is a list, add to an existing array, otherwise use hash syntax
- # +current_key+ main model that will be a key in the hash
- # +model_object+ model or list of models to include in the hash
- def process_sub_model(current_key, model_object)
- sub_model_json = build_json_config_hash(model_object).dup
- @json_config_hash.slice!(current_key)
-
- if @json_config_hash[current_key] && @json_config_hash[current_key][:include]
- @json_config_hash[current_key][:include] << sub_model_json
- else
- @json_config_hash[current_key] = { include: sub_model_json }
- end
- end
-
- # Creates or adds to an existing hash an individual model or list
- # +current_key+ main model that will be a key in the hash
- # +model_object+ model or list of models to include in the hash
- def handle_model_object(current_key, model_object)
- if @json_config_hash[current_key]
- add_model_value(current_key, model_object)
- else
- create_model_value(current_key, model_object)
- end
- end
-
- # Constructs a new hash that will hold the configuration for that particular object
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- def create_model_value(current_key, value)
- parsed_hash = { include: value }
-
- @attributes_finder.parse(value) do |hash|
- parsed_hash = { include: hash_or_merge(value, hash) }
- end
- @json_config_hash[current_key] = parsed_hash
- end
-
- # Adds new model configuration to an existing hash with key +current_key+
- # It may include exceptions or other attribute detail configuration, parsed by +@attributes_finder+
- # +current_key+ main model that will be a key in the hash
- # +value+ existing model to be included in the hash
- def add_model_value(current_key, value)
- @attributes_finder.parse(value) { |hash| value = { value => hash } }
- old_values = @json_config_hash[current_key][:include]
- @json_config_hash[current_key][:include] = ([old_values] + [value]).compact.flatten
- end
-
- # Construct a new hash or merge with an existing one a model configuration
- # This is to fulfil +to_json+ requirements.
- # +value+ existing model to be included in the hash
- # +hash+ hash containing configuration generated mainly from +@attributes_finder+
- def hash_or_merge(value, hash)
- value.is_a?(Hash) ? value.merge(hash) : { value => hash }
- end
end
end
end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 9824df3f274..b0726268ca6 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -13,6 +13,10 @@ module Gitlab
BUILD_MODELS = %w[Ci::Build commit_status].freeze
+ IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
+
+ EXISTING_OBJECT_CHECK = %i[milestone milestones label labels].freeze
+
def self.create(*args)
new(*args).create
end
@@ -22,23 +26,35 @@ module Gitlab
@relation_hash = relation_hash.except('id', 'noteable_id')
@members_mapper = members_mapper
@user = user
+ @imported_object_retries = 0
end
# Creates an object from an actual model with name "relation_sym" with params from
# the relation_hash, updating references with new object IDs, mapping users using
# the "members_mapper" object, also updating notes if required.
def create
- set_note_author if @relation_name == :notes
- update_user_references
- update_project_references
- reset_ci_tokens if @relation_name == 'Ci::Trigger'
- @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
+ setup_models
generate_imported_object
end
private
+ def setup_models
+ if @relation_name == :notes
+ set_note_author
+
+ # attachment is deprecated and note uploads are handled by Markdown uploader
+ @relation_hash['attachment'] = nil
+ end
+
+ update_user_references
+ update_project_references
+ reset_ci_tokens if @relation_name == 'Ci::Trigger'
+ @relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
+ set_st_diffs if @relation_name == :merge_request_diff
+ end
+
def update_user_references
USER_REFERENCES.each do |reference|
if @relation_hash[reference]
@@ -86,17 +102,19 @@ module Gitlab
def update_project_references
project_id = @relation_hash.delete('project_id')
+ # If source and target are the same, populate them with the new project ID.
+ if @relation_hash['source_project_id']
+ @relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1
+ end
+
# project_id may not be part of the export, but we always need to populate it if required.
- @relation_hash['project_id'] = project_id if relation_class.column_names.include?('project_id')
+ @relation_hash['project_id'] = project_id
@relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
- @relation_hash['source_project_id'] = -1 if @relation_hash['source_project_id']
+ end
- # If source and target are the same, populate them with the new project ID.
- if @relation_hash['source_project_id'] && @relation_hash['target_project_id'] &&
- @relation_hash['target_project_id'] == @relation_hash['source_project_id']
- @relation_hash['source_project_id'] = project_id
- end
+ def same_source_and_target?
+ @relation_hash['target_project_id'] && @relation_hash['target_project_id'] == @relation_hash['source_project_id']
end
def reset_ci_tokens
@@ -111,10 +129,14 @@ module Gitlab
end
def imported_object
- imported_object = relation_class.new(@relation_hash)
- yield(imported_object) if block_given?
- imported_object.importing = true if imported_object.respond_to?(:importing)
- imported_object
+ yield(existing_or_new_object) if block_given?
+ existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing)
+ existing_or_new_object
+ rescue ActiveRecord::RecordNotUnique
+ # as the operation is not atomic, retry in the unlikely scenario an INSERT is
+ # performed on the same object between the SELECT and the INSERT
+ @imported_object_retries += 1
+ retry if @imported_object_retries < IMPORTED_OBJECT_MAX_RETRIES
end
def update_note_for_missing_author(author_name)
@@ -125,6 +147,28 @@ module Gitlab
def admin_user?
@user.is_admin?
end
+
+ def parsed_relation_hash
+ @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ end
+
+ def set_st_diffs
+ @relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
+ end
+
+ def existing_or_new_object
+ # Only find existing records to avoid mapping tables such as milestones
+ # Otherwise always create the record, skipping the extra SELECT clause.
+ @existing_or_new_object ||= begin
+ if EXISTING_OBJECT_CHECK.include?(@relation_name)
+ existing_object = relation_class.find_or_initialize_by(parsed_relation_hash.slice('title', 'project_id'))
+ existing_object.assign_attributes(parsed_relation_hash)
+ existing_object
+ else
+ relation_class.new(parsed_relation_hash)
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 546dae4d122..6d9379acf25 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -3,19 +3,18 @@ module Gitlab
class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil
- def initialize(project:, shared:, path_to_bundle:, wiki: false)
+ def initialize(project:, shared:, path_to_bundle:)
@project = project
@path_to_bundle = path_to_bundle
@shared = shared
- @wiki = wiki
end
def restore
- return wiki? unless File.exist?(@path_to_bundle)
+ return true unless File.exist?(@path_to_bundle)
FileUtils.mkdir_p(path_to_repo)
- git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle)
+ git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e
@shared.error(e)
false
@@ -31,8 +30,14 @@ module Gitlab
@project.repository.path_to_repo
end
+ def repo_restore_hooks
+ return true if wiki?
+
+ git_restore_hooks
+ end
+
def wiki?
- @wiki
+ @project.class.name == 'ProjectWiki'
end
end
end
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index cce43fe994b..331e14021e6 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -11,7 +11,7 @@ module Gitlab
end
def save
- return false if @project.empty_repo?
+ return true if @project.empty_repo? # it's ok to have no repo
@full_path = File.join(@shared.export_path, ImportExport.project_bundle_filename)
bundle_to_disk
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index 6a60b65071f..6130c124dd1 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -7,7 +7,8 @@ module Gitlab
new(*args).save
end
- def initialize(shared:)
+ def initialize(project:, shared:)
+ @project = project
@shared = shared
end
@@ -36,7 +37,7 @@ module Gitlab
end
def archive_file
- @archive_file ||= File.join(@shared.export_path, '..', "#{Time.now.strftime('%Y-%m-%d_%H-%M-%3N')}_project_export.tar.gz")
+ @archive_file ||= File.join(@shared.export_path, '..', Gitlab::ImportExport.export_filename(project: @project))
end
end
end
diff --git a/lib/gitlab/import_export/uploads_saver.rb b/lib/gitlab/import_export/uploads_saver.rb
index d6f4fa57510..62a2553675c 100644
--- a/lib/gitlab/import_export/uploads_saver.rb
+++ b/lib/gitlab/import_export/uploads_saver.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
class UploadsSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
def initialize(project:, shared:)
@project = project
@shared = shared
@@ -17,12 +19,6 @@ module Gitlab
private
- def copy_files(source, destination)
- FileUtils.mkdir_p(destination)
- FileUtils.copy_entry(source, destination)
- true
- end
-
def uploads_export_path
File.join(@shared.export_path, 'uploads')
end
diff --git a/lib/gitlab/import_export/version_checker.rb b/lib/gitlab/import_export/version_checker.rb
index abfc694b879..de3fe6d822e 100644
--- a/lib/gitlab/import_export/version_checker.rb
+++ b/lib/gitlab/import_export/version_checker.rb
@@ -25,7 +25,7 @@ module Gitlab
def verify_version!(version)
if Gem::Version.new(version) > Gem::Version.new(Gitlab::ImportExport.version)
- raise Gitlab::ImportExport::Error("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ raise Gitlab::ImportExport::Error.new("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
else
true
end
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 1eedae39f8a..6107420e4dd 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -4,6 +4,7 @@ module Gitlab
def save
@wiki = ProjectWiki.new(@project)
return true unless wiki_repository_exists? # it's okay to have no Wiki
+
bundle_to_disk(File.join(@shared.export_path, project_filename))
end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 59a05411fe9..94261b7eeed 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -14,13 +14,12 @@ module Gitlab
def options
{
- 'GitHub' => 'github',
- 'Bitbucket' => 'bitbucket',
- 'GitLab.com' => 'gitlab',
- 'Gitorious.org' => 'gitorious',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
- 'Repo by URL' => 'git',
+ 'GitHub' => 'github',
+ 'Bitbucket' => 'bitbucket',
+ 'GitLab.com' => 'gitlab',
+ 'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
+ 'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project'
}
end
diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb
index 8ce9d32abe0..d7be50bd437 100644
--- a/lib/gitlab/incoming_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,7 +1,7 @@
module Gitlab
module IncomingEmail
class << self
- FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
+ FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
def enabled?
config.enabled && config.address
@@ -21,8 +21,8 @@ module Gitlab
match[1]
end
- def key_from_fallback_reply_message_id(message_id)
- match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
+ def key_from_fallback_message_id(mail_id)
+ match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX)
return unless match
match[1]
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index f2b649e50a2..2f326d00a2f 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -25,7 +25,7 @@ module Gitlab
end
end
- def initialize(user, adapter=nil)
+ def initialize(user, adapter = nil)
@adapter = adapter
@user = user
@provider = user.ldap_identity.provider
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index df65179bfea..9a5bcfb5c9b 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -13,7 +13,7 @@ module Gitlab
Gitlab::LDAP::Config.new(provider)
end
- def initialize(provider, ldap=nil)
+ def initialize(provider, ldap = nil)
@provider = provider
@ldap = ldap || Net::LDAP.new(config.adapter_options)
end
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
deleted file mode 100644
index 811363405a8..00000000000
--- a/lib/gitlab/lfs/response.rb
+++ /dev/null
@@ -1,327 +0,0 @@
-module Gitlab
- module Lfs
- class Response
- def initialize(project, user, ci, request)
- @origin_project = project
- @project = storage_project(project)
- @user = user
- @ci = ci
- @env = request.env
- @request = request
- end
-
- def render_download_object_response(oid)
- render_response_to_download do
- if check_download_sendfile_header?
- render_lfs_sendfile(oid)
- else
- render_not_found
- end
- end
- end
-
- def render_batch_operation_response
- request_body = JSON.parse(@request.body.read)
- case request_body["operation"]
- when "download"
- render_batch_download(request_body)
- when "upload"
- render_batch_upload(request_body)
- else
- render_not_found
- end
- end
-
- def render_storage_upload_authorize_response(oid, size)
- render_response_to_push do
- [
- 200,
- { "Content-Type" => "application/json; charset=utf-8" },
- [JSON.dump({
- 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
- 'LfsOid' => oid,
- 'LfsSize' => size
- })]
- ]
- end
- end
-
- def render_storage_upload_store_response(oid, size, tmp_file_name)
- render_response_to_push do
- render_lfs_upload_ok(oid, size, tmp_file_name)
- end
- end
-
- def render_unsupported_deprecated_api
- [
- 501,
- { "Content-Type" => "application/json; charset=utf-8" },
- [JSON.dump({
- 'message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
- 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
- })]
- ]
- end
-
- private
-
- def render_not_enabled
- [
- 501,
- {
- "Content-Type" => "application/json; charset=utf-8",
- },
- [JSON.dump({
- 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
- 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
- })]
- ]
- end
-
- def render_unauthorized
- [
- 401,
- {
- 'Content-Type' => 'text/plain'
- },
- ['Unauthorized']
- ]
- end
-
- def render_not_found
- [
- 404,
- {
- "Content-Type" => "application/vnd.git-lfs+json"
- },
- [JSON.dump({
- 'message' => 'Not found.',
- 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
- })]
- ]
- end
-
- def render_forbidden
- [
- 403,
- {
- "Content-Type" => "application/vnd.git-lfs+json"
- },
- [JSON.dump({
- 'message' => 'Access forbidden. Check your access level.',
- 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
- })]
- ]
- end
-
- def render_lfs_sendfile(oid)
- return render_not_found unless oid.present?
-
- lfs_object = object_for_download(oid)
-
- if lfs_object && lfs_object.file.exists?
- [
- 200,
- {
- # GitLab-workhorse will forward Content-Type header
- "Content-Type" => "application/octet-stream",
- "X-Sendfile" => lfs_object.file.path
- },
- []
- ]
- else
- render_not_found
- end
- end
-
- def render_batch_upload(body)
- return render_not_found if body.empty? || body['objects'].nil?
-
- render_response_to_push do
- response = build_upload_batch_response(body['objects'])
- [
- 200,
- {
- "Content-Type" => "application/json; charset=utf-8",
- "Cache-Control" => "private",
- },
- [JSON.dump(response)]
- ]
- end
- end
-
- def render_batch_download(body)
- return render_not_found if body.empty? || body['objects'].nil?
-
- render_response_to_download do
- response = build_download_batch_response(body['objects'])
- [
- 200,
- {
- "Content-Type" => "application/json; charset=utf-8",
- "Cache-Control" => "private",
- },
- [JSON.dump(response)]
- ]
- end
- end
-
- def render_lfs_upload_ok(oid, size, tmp_file)
- if store_file(oid, size, tmp_file)
- [
- 200,
- {
- 'Content-Type' => 'text/plain',
- 'Content-Length' => 0
- },
- []
- ]
- else
- [
- 422,
- { 'Content-Type' => 'text/plain' },
- ["Unprocessable entity"]
- ]
- end
- end
-
- def render_response_to_download
- return render_not_enabled unless Gitlab.config.lfs.enabled
-
- unless @project.public?
- return render_unauthorized unless @user || @ci
- return render_forbidden unless user_can_fetch?
- end
-
- yield
- end
-
- def render_response_to_push
- return render_not_enabled unless Gitlab.config.lfs.enabled
- return render_unauthorized unless @user
- return render_forbidden unless user_can_push?
-
- yield
- end
-
- def check_download_sendfile_header?
- @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
- end
-
- def user_can_fetch?
- # Check user access against the project they used to initiate the pull
- @ci || @user.can?(:download_code, @origin_project)
- end
-
- def user_can_push?
- # Check user access against the project they used to initiate the push
- @user.can?(:push_code, @origin_project)
- end
-
- def storage_project(project)
- if project.forked?
- storage_project(project.forked_from_project)
- else
- project
- end
- end
-
- def store_file(oid, size, tmp_file)
- tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
-
- object = LfsObject.find_or_create_by(oid: oid, size: size)
- if object.file.exists?
- success = true
- else
- success = move_tmp_file_to_storage(object, tmp_file_path)
- end
-
- if success
- success = link_to_project(object)
- end
-
- success
- ensure
- # Ensure that the tmp file is removed
- FileUtils.rm_f(tmp_file_path)
- end
-
- def object_for_download(oid)
- @project.lfs_objects.find_by(oid: oid)
- end
-
- def move_tmp_file_to_storage(object, path)
- File.open(path) do |f|
- object.file = f
- end
-
- object.file.store!
- object.save
- end
-
- def link_to_project(object)
- if object && !object.projects.exists?(@project.id)
- object.projects << @project
- object.save
- end
- end
-
- def select_existing_objects(objects)
- objects_oids = objects.map { |o| o['oid'] }
- @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
- end
-
- def build_upload_batch_response(objects)
- selected_objects = select_existing_objects(objects)
-
- upload_hypermedia_links(objects, selected_objects)
- end
-
- def build_download_batch_response(objects)
- selected_objects = select_existing_objects(objects)
-
- download_hypermedia_links(objects, selected_objects)
- end
-
- def download_hypermedia_links(all_objects, existing_objects)
- all_objects.each do |object|
- if existing_objects.include?(object['oid'])
- object['actions'] = {
- 'download' => {
- 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
- 'header' => {
- 'Authorization' => @env['HTTP_AUTHORIZATION']
- }.compact
- }
- }
- else
- object['error'] = {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- end
- end
-
- { 'objects' => all_objects }
- end
-
- def upload_hypermedia_links(all_objects, existing_objects)
- all_objects.each do |object|
- # generate actions only for non-existing objects
- next if existing_objects.include?(object['oid'])
-
- object['actions'] = {
- 'upload' => {
- 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
- 'header' => {
- 'Authorization' => @env['HTTP_AUTHORIZATION']
- }.compact
- }
- }
- end
-
- { 'objects' => all_objects }
- end
- end
- end
-end
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
deleted file mode 100644
index 69bd5e62305..00000000000
--- a/lib/gitlab/lfs/router.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-module Gitlab
- module Lfs
- class Router
- attr_reader :project, :user, :ci, :request
-
- def initialize(project, user, ci, request)
- @project = project
- @user = user
- @ci = ci
- @env = request.env
- @request = request
- end
-
- def try_call
- return unless @request && @request.path.present?
-
- case @request.request_method
- when 'GET'
- get_response
- when 'POST'
- post_response
- when 'PUT'
- put_response
- else
- nil
- end
- end
-
- private
-
- def get_response
- path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
- return nil unless path_match
-
- oid = path_match[2]
- return nil unless oid
-
- case path_match[1]
- when "info/lfs"
- lfs.render_unsupported_deprecated_api
- when "gitlab-lfs"
- lfs.render_download_object_response(oid)
- else
- nil
- end
- end
-
- def post_response
- post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
- return nil unless post_path
-
- # Check for Batch API
- if post_path[0].ends_with?("/info/lfs/objects/batch")
- lfs.render_batch_operation_response
- elsif post_path[0].ends_with?("/info/lfs/objects")
- lfs.render_unsupported_deprecated_api
- else
- nil
- end
- end
-
- def put_response
- object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
- return nil if object_match.nil?
-
- oid = object_match[1]
- size = object_match[2].try(:to_i)
- return nil if oid.nil? || size.nil?
-
- # GitLab-workhorse requests
- # 1. Try to authorize the request
- # 2. send a request with a header containing the name of the temporary file
- if object_match[3] && object_match[3] == '/authorize'
- lfs.render_storage_upload_authorize_response(oid, size)
- else
- tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
- return nil unless tmp_file_name
-
- lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
- end
- end
-
- def lfs
- return unless @project
-
- Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
- end
-
- def sanitize_tmp_filename(name)
- if name.present?
- name.gsub!(/^.*(\\|\/)/, '')
- name = name.match(/[0-9a-f]{73}/)
- name[0] if name
- else
- nil
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
new file mode 100644
index 00000000000..12999a90a29
--- /dev/null
+++ b/lib/gitlab/mail_room.rb
@@ -0,0 +1,47 @@
+require 'yaml'
+require 'json'
+require_relative 'redis' unless defined?(Gitlab::Redis)
+
+module Gitlab
+ module MailRoom
+ class << self
+ def enabled?
+ config[:enabled] && config[:address]
+ end
+
+ def config
+ @config ||= fetch_config
+ end
+
+ def reset_config!
+ @config = nil
+ end
+
+ private
+
+ def fetch_config
+ return {} unless File.exist?(config_file)
+
+ rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
+ all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys
+
+ config = all_config[:incoming_email] || {}
+ config[:enabled] = false if config[:enabled].nil?
+ config[:port] = 143 if config[:port].nil?
+ config[:ssl] = false if config[:ssl].nil?
+ config[:start_tls] = false if config[:start_tls].nil?
+ config[:mailbox] = 'inbox' if config[:mailbox].nil?
+
+ if config[:enabled] && config[:address]
+ config[:redis_url] = Gitlab::Redis.new(rails_env).url
+ end
+
+ config
+ end
+
+ def config_file
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 49f702f91f6..3d1ba33ec68 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -124,6 +124,20 @@ module Gitlab
trans.action = action if trans
end
+ # Tracks an event.
+ #
+ # See `Gitlab::Metrics::Transaction#add_event` for more details.
+ def self.add_event(*args)
+ trans = current_transaction
+
+ trans.add_event(*args) if trans
+ end
+
+ # Returns the prefix to use for the name of a series.
+ def self.series_prefix
+ @series_prefix ||= Sidekiq.server? ? 'sidekiq_' : 'rails_'
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
@@ -136,8 +150,7 @@ module Gitlab
end
end
- private
-
+ # Allow access from other metrics related middlewares
def self.current_transaction
Transaction.current
end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index dcec7543c13..4b7a791e497 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -9,14 +9,17 @@ module Gitlab
#
# Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
module Instrumentation
- SERIES = 'method_calls'
-
PROXY_IVAR = :@__gitlab_instrumentation_proxy
def self.configure
yield self
end
+ # Returns the name of the series to use for storing method calls.
+ def self.series
+ @series ||= "#{Metrics.series_prefix}method_calls"
+ end
+
# Instruments a class method.
#
# mod - The module to instrument as a Module/Class.
@@ -141,15 +144,15 @@ module Gitlab
# generated method _only_ accepts regular arguments if the underlying
# method also accepts them.
if method.arity == 0
- args_signature = '&block'
+ args_signature = ''
else
- args_signature = '*args, &block'
+ args_signature = '*args'
end
proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
def #{name}(#{args_signature})
if trans = Gitlab::Metrics::Instrumentation.transaction
- trans.measure_method(#{label.inspect}) { super }
+ trans.method_call_for(#{label.to_sym.inspect}).measure { super }
else
super
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index c048fe20ba7..d3465e5ec19 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -11,8 +11,8 @@ module Gitlab
def initialize(name, series)
@name = name
@series = series
- @real_time = 0.0
- @cpu_time = 0.0
+ @real_time = 0
+ @cpu_time = 0
@call_count = 0
end
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index f23d67e1e38..bd0afe53c51 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -4,15 +4,20 @@ module Gitlab
class Metric
JITTER_RANGE = 0.000001..0.001
- attr_reader :series, :values, :tags
+ attr_reader :series, :values, :tags, :type
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
- def initialize(series, values, tags = {})
+ def initialize(series, values, tags = {}, type = :metric)
@values = values
@series = series
@tags = tags
+ @type = type
+ end
+
+ def event?
+ type == :event
end
# Returns a Hash in a format that can be directly written to InfluxDB.
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index e61670f491c..01c96a6fe96 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -4,6 +4,17 @@ module Gitlab
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
ENDPOINT_KEY = 'api.endpoint'
+ CONTENT_TYPES = {
+ 'text/html' => :html,
+ 'text/plain' => :txt,
+ 'application/json' => :json,
+ 'text/js' => :js,
+ 'application/atom+xml' => :atom,
+ 'image/png' => :png,
+ 'image/jpeg' => :jpeg,
+ 'image/gif' => :gif,
+ 'image/svg+xml' => :svg
+ }
def initialize(app)
@app = app
@@ -17,6 +28,10 @@ module Gitlab
begin
retval = trans.run { @app.call(env) }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:rails_exception)
+
+ raise error
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
@@ -42,8 +57,15 @@ module Gitlab
end
def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- trans.action = "#{controller.class.name}##{controller.action_name}"
+ controller = env[CONTROLLER_KEY]
+ action = "#{controller.class.name}##{controller.action_name}"
+ suffix = CONTENT_TYPES[controller.content_type]
+
+ if suffix && suffix != :html
+ action += ".#{suffix}"
+ end
+
+ trans.action = action
end
def tag_endpoint(trans, env)
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index a1240fd33ee..f9dd8e41912 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -11,6 +11,10 @@ module Gitlab
# Old gitlad-shell messages don't provide enqueued_at/created_at attributes
trans.set(:sidekiq_queue_duration, Time.now.to_f - (message['enqueued_at'] || message['created_at'] || 0))
trans.run { yield }
+ rescue Exception => error # rubocop: disable Lint/RescueException
+ trans.add_event(:sidekiq_exception)
+
+ raise error
ensure
trans.finish
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 82c18bb108b..287b7a83547 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -35,12 +35,12 @@ module Gitlab
if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)
def self.cpu_time
Process.
- clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond).to_f
+ clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
end
else
def self.cpu_time
Process.
- clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond).to_f
+ clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond)
end
end
@@ -48,14 +48,14 @@ module Gitlab
#
# Returns the time as a Float.
def self.real_time(precision = :millisecond)
- Process.clock_gettime(Process::CLOCK_REALTIME, precision).to_f
+ Process.clock_gettime(Process::CLOCK_REALTIME, precision)
end
# Returns the current monotonic clock time in a given precision.
#
# Returns the time as a Float.
def self.monotonic_time(precision = :millisecond)
- Process.clock_gettime(Process::CLOCK_MONOTONIC, precision).to_f
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, precision)
end
end
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index bded245da43..7bc16181be6 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,7 +4,10 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- attr_reader :tags, :values, :methods
+ # The series to store events (e.g. Git pushes) in.
+ EVENT_SERIES = 'events'
+
+ attr_reader :tags, :values, :method, :metrics
attr_accessor :action
@@ -52,23 +55,30 @@ module Gitlab
end
def add_metric(series, values, tags = {})
- @metrics << Metric.new("#{series_prefix}#{series}", values, tags)
+ @metrics << Metric.new("#{Metrics.series_prefix}#{series}", values, tags)
end
- # Measures the time it takes to execute a method.
+ # Tracks a business level event
#
- # Multiple calls to the same method add up to the total runtime of the
- # method.
+ # Business level events including events such as Git pushes, Emails being
+ # sent, etc.
#
- # name - The full name of the method to measure (e.g. `User#sign_in`).
- def measure_method(name, &block)
- unless @methods[name]
- series = "#{series_prefix}#{Instrumentation::SERIES}"
+ # event_name - The name of the event (e.g. "git_push").
+ # tags - A set of tags to attach to the event.
+ def add_event(event_name, tags = {})
+ @metrics << Metric.new(EVENT_SERIES,
+ { count: 1 },
+ { event: event_name }.merge(tags),
+ :event)
+ end
- @methods[name] = MethodCall.new(name, series)
+ # Returns a MethodCall object for the given name.
+ def method_call_for(name)
+ unless method = @methods[name]
+ @methods[name] = method = MethodCall.new(name, Instrumentation.series)
end
- @methods[name].measure(&block)
+ method
end
def increment(name, value)
@@ -108,21 +118,13 @@ module Gitlab
submit_hashes = submit.map do |metric|
hash = metric.to_hash
- hash[:tags][:action] ||= @action if @action
+ hash[:tags][:action] ||= @action if @action && !metric.event?
hash
end
Metrics.submit_metrics(submit_hashes)
end
-
- def sidekiq?
- Sidekiq.server?
- end
-
- def series_prefix
- sidekiq? ? 'sidekiq_' : 'rails_'
- end
end
end
end
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 56608b1b276..5d2d7d0026c 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -11,7 +11,7 @@ module Gitlab
def call(env)
trans = Gitlab::Metrics.current_transaction
- proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence
+ proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
if trans && proxy_start
# Time in milliseconds since gitlab-workhorse started the request
trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
deleted file mode 100644
index 8bdc89a7751..00000000000
--- a/lib/gitlab/note_data_builder.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-module Gitlab
- class NoteDataBuilder
- class << self
- # Produce a hash of post-receive data
- #
- # For all notes:
- #
- # data = {
- # object_kind: "note",
- # user: {
- # name: String,
- # username: String,
- # avatar_url: String
- # }
- # project_id: Integer,
- # repository: {
- # name: String,
- # url: String,
- # description: String,
- # homepage: String,
- # }
- # object_attributes: {
- # <hook data for note>
- # }
- # <note-specific data>: {
- # }
- # note-specific data is a hash with one of the following keys and contains
- # the hook data for that type.
- # - commit
- # - issue
- # - merge_request
- # - snippet
- #
- def build(note, user)
- project = note.project
- data = build_base_data(project, user, note)
-
- if note.for_commit?
- data[:commit] = build_data_for_commit(project, user, note)
- elsif note.for_issue?
- data[:issue] = note.noteable.hook_attrs
- elsif note.for_merge_request?
- data[:merge_request] = note.noteable.hook_attrs
- elsif note.for_snippet?
- data[:snippet] = note.noteable.hook_attrs
- end
-
- data
- end
-
- def build_base_data(project, user, note)
- base_data = {
- object_kind: "note",
- user: user.hook_attrs,
- project_id: project.id,
- project: project.hook_attrs,
- object_attributes: note.hook_attrs,
- # DEPRECATED
- repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
- }
-
- base_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(note)
- base_data
- end
-
- def build_data_for_commit(project, user, note)
- # commit_id is the SHA hash
- commit = project.commit(note.commit_id)
- commit.hook_attrs
- end
- end
- end
-end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index 43e07e09160..ca23ccef25b 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,7 +5,7 @@ module Gitlab
module Popen
extend self
- def popen(cmd, path=nil)
+ def popen(cmd, path = nil)
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
deleted file mode 100644
index c8f12577112..00000000000
--- a/lib/gitlab/push_data_builder.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-module Gitlab
- class PushDataBuilder
- class << self
- # Produce a hash of post-receive data
- #
- # data = {
- # before: String,
- # after: String,
- # ref: String,
- # user_id: String,
- # user_name: String,
- # user_email: String
- # project_id: String,
- # repository: {
- # name: String,
- # url: String,
- # description: String,
- # homepage: String,
- # },
- # commits: Array,
- # total_commits_count: Fixnum
- # }
- #
- def build(project, user, oldrev, newrev, ref, commits = [], message = nil)
- commits = Array(commits)
-
- # Total commits count
- commits_count = commits.size
-
- # Get latest 20 commits ASC
- commits_limited = commits.last(20)
-
- # For performance purposes maximum 20 latest commits
- # will be passed as post receive hook data.
- commit_attrs = commits_limited.map do |commit|
- commit.hook_attrs(with_changed_files: true)
- end
-
- type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
-
- # Hash to be passed as post_receive_data
- data = {
- object_kind: type,
- event_name: type,
- before: oldrev,
- after: newrev,
- ref: ref,
- checkout_sha: checkout_sha(project.repository, newrev, ref),
- message: message,
- user_id: user.id,
- user_name: user.name,
- user_email: user.email,
- user_avatar: user.avatar_url,
- project_id: project.id,
- project: project.hook_attrs,
- commits: commit_attrs,
- total_commits_count: commits_count,
- # DEPRECATED
- repository: project.hook_attrs.slice(:name, :url, :description, :homepage,
- :git_http_url, :git_ssh_url, :visibility_level)
- }
-
- data
- end
-
- # This method provide a sample data generated with
- # existing project and commits to test webhooks
- def build_sample(project, user)
- commits = project.repository.commits(project.default_branch, limit: 3)
- ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
- build(project, user, commits.last.id, commits.first.id, ref, commits)
- end
-
- def checkout_sha(repository, newrev, ref)
- # Checkout sha is nil when we remove branch or tag
- return if Gitlab::Git.blank_ref?(newrev)
-
- # Find sha for tag, except when it was deleted.
- if Gitlab::Git.tag_ref?(ref)
- tag_name = Gitlab::Git.ref_name(ref)
- tag = repository.find_tag(tag_name)
-
- if tag
- commit = repository.commit(tag.target)
- commit.try(:sha)
- end
- else
- newrev
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 40766f35f77..9376b54f43b 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,50 +1,94 @@
+# This file should not have any direct dependency on Rails environment
+# please require all dependencies below:
+require 'active_support/core_ext/hash/keys'
+
module Gitlab
class Redis
CACHE_NAMESPACE = 'cache:gitlab'
SESSION_NAMESPACE = 'session:gitlab'
SIDEKIQ_NAMESPACE = 'resque:gitlab'
-
- attr_reader :url
+ MAILROOM_NAMESPACE = 'mail_room:gitlab'
+ DEFAULT_REDIS_URL = 'redis://localhost:6379'
# To be thread-safe we must be careful when writing the class instance
# variables @url and @pool. Because @pool depends on @url we need two
# mutexes to prevent deadlock.
- URL_MUTEX = Mutex.new
+ PARAMS_MUTEX = Mutex.new
POOL_MUTEX = Mutex.new
- private_constant :URL_MUTEX, :POOL_MUTEX
+ private_constant :PARAMS_MUTEX, :POOL_MUTEX
- def self.url
- @url || URL_MUTEX.synchronize { @url = new.url }
- end
+ class << self
+ def params
+ @params || PARAMS_MUTEX.synchronize { @params = new.params }
+ end
+
+ # @deprecated Use .params instead to get sentinel support
+ def url
+ new.url
+ end
- def self.with
- if @pool.nil?
- POOL_MUTEX.synchronize do
- @pool = ConnectionPool.new { ::Redis.new(url: url) }
+ def with
+ if @pool.nil?
+ POOL_MUTEX.synchronize do
+ @pool = ConnectionPool.new { ::Redis.new(params) }
+ end
end
+ @pool.with { |redis| yield redis }
+ end
+
+ def reset_params!
+ @params = nil
end
- @pool.with { |redis| yield redis }
end
- def self.redis_store_options
- url = new.url
- redis_config_hash = ::Redis::Store::Factory.extract_host_options_from_uri(url)
- # Redis::Store does not handle Unix sockets well, so let's do it for them
- redis_uri = URI.parse(url)
+ def initialize(rails_env = nil)
+ @rails_env = rails_env || ::Rails.env
+ end
+
+ def params
+ redis_store_options
+ end
+
+ def url
+ raw_config_hash[:url]
+ end
+
+ private
+
+ def redis_store_options
+ config = raw_config_hash
+ redis_url = config.delete(:url)
+ redis_uri = URI.parse(redis_url)
+
if redis_uri.scheme == 'unix'
- redis_config_hash[:path] = redis_uri.path
+ # Redis::Store does not handle Unix sockets well, so let's do it for them
+ config[:path] = redis_uri.path
+ config
+ else
+ redis_hash = ::Redis::Store::Factory.extract_host_options_from_uri(redis_url)
+ # order is important here, sentinels must be after the connection keys.
+ # {url: ..., port: ..., sentinels: [...]}
+ redis_hash.merge(config)
end
- redis_config_hash
end
- def initialize(rails_env=nil)
- rails_env ||= Rails.env
- config_file = File.expand_path('../../../config/resque.yml', __FILE__)
+ def raw_config_hash
+ config_data = fetch_config
- @url = "redis://localhost:6379"
- if File.exist?(config_file)
- @url = YAML.load_file(config_file)[rails_env]
+ if config_data
+ config_data.is_a?(String) ? { url: config_data } : config_data.deep_symbolize_keys
+ else
+ { url: DEFAULT_REDIS_URL }
end
end
+
+ def fetch_config
+ file = config_file
+ File.exist?(file) ? YAML.load_file(file)[@rails_env] : false
+ end
+
+ def config_file
+ File.expand_path('../../../config/resque.yml', __FILE__)
+ end
end
end
diff --git a/lib/gitlab/request_profiler.rb b/lib/gitlab/request_profiler.rb
new file mode 100644
index 00000000000..8130e55351e
--- /dev/null
+++ b/lib/gitlab/request_profiler.rb
@@ -0,0 +1,19 @@
+require 'fileutils'
+
+module Gitlab
+ module RequestProfiler
+ PROFILES_DIR = "#{Gitlab.config.shared.path}/tmp/requests_profiles"
+
+ def profile_token
+ Rails.cache.fetch('profile-token') do
+ Devise.friendly_token
+ end
+ end
+ module_function :profile_token
+
+ def remove_all_profiles
+ FileUtils.rm_rf(PROFILES_DIR)
+ end
+ module_function :remove_all_profiles
+ end
+end
diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb
new file mode 100644
index 00000000000..786e1d49f5e
--- /dev/null
+++ b/lib/gitlab/request_profiler/middleware.rb
@@ -0,0 +1,54 @@
+require 'ruby-prof'
+require_dependency 'gitlab/request_profiler'
+
+module Gitlab
+ module RequestProfiler
+ class Middleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if profile?(env)
+ call_with_profiling(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ def profile?(env)
+ header_token = env['HTTP_X_PROFILE_TOKEN']
+ return unless header_token.present?
+
+ profile_token = RequestProfiler.profile_token
+ return unless profile_token.present?
+
+ header_token == profile_token
+ end
+
+ def call_with_profiling(env)
+ ret = nil
+ result = RubyProf::Profile.profile do
+ ret = catch(:warden) do
+ @app.call(env)
+ end
+ end
+
+ printer = RubyProf::CallStackPrinter.new(result)
+ file_name = "#{env['PATH_INFO'].tr('/', '|')}_#{Time.current.to_i}.html"
+ file_path = "#{PROFILES_DIR}/#{file_name}"
+
+ FileUtils.mkdir_p(PROFILES_DIR)
+ File.open(file_path, 'wb') do |file|
+ printer.print(file)
+ end
+
+ if ret.is_a?(Array)
+ ret
+ else
+ throw(:warden, ret)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/request_profiler/profile.rb b/lib/gitlab/request_profiler/profile.rb
new file mode 100644
index 00000000000..f89d56903ef
--- /dev/null
+++ b/lib/gitlab/request_profiler/profile.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module RequestProfiler
+ class Profile
+ attr_reader :name, :time, :request_path
+
+ alias_method :to_param, :name
+
+ def self.all
+ Dir["#{PROFILES_DIR}/*.html"].map do |path|
+ new(File.basename(path))
+ end
+ end
+
+ def self.find(name)
+ name_dup = name.dup
+ name_dup << '.html' unless name.end_with?('.html')
+
+ file_path = "#{PROFILES_DIR}/#{name_dup}"
+ return unless File.exist?(file_path)
+
+ new(name_dup)
+ end
+
+ def initialize(name)
+ @name = name
+
+ set_attributes
+ end
+
+ def content
+ File.read("#{PROFILES_DIR}/#{name}")
+ end
+
+ private
+
+ def set_attributes
+ _, path, timestamp = name.split(/(.*)_(\d+)\.html$/)
+ @request_path = path.tr('|', '/')
+ @time = Time.at(timestamp.to_i).utc
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_middleware/request_store_middleware.rb b/lib/gitlab/sidekiq_middleware/request_store_middleware.rb
new file mode 100644
index 00000000000..b1fa0e3cb4e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/request_store_middleware.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module SidekiqMiddleware
+ class RequestStoreMiddleware
+ def call(worker, job, queue)
+ RequestStore.begin!
+ yield
+ ensure
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/command_definition.rb b/lib/gitlab/slash_commands/command_definition.rb
new file mode 100644
index 00000000000..60d35be2599
--- /dev/null
+++ b/lib/gitlab/slash_commands/command_definition.rb
@@ -0,0 +1,57 @@
+module Gitlab
+ module SlashCommands
+ class CommandDefinition
+ attr_accessor :name, :aliases, :description, :params, :condition_block, :action_block
+
+ def initialize(name, attributes = {})
+ @name = name
+
+ @aliases = attributes[:aliases] || []
+ @description = attributes[:description] || ''
+ @params = attributes[:params] || []
+ @condition_block = attributes[:condition_block]
+ @action_block = attributes[:action_block]
+ end
+
+ def all_names
+ [name, *aliases]
+ end
+
+ def noop?
+ action_block.nil?
+ end
+
+ def available?(opts)
+ return true unless condition_block
+
+ context = OpenStruct.new(opts)
+ context.instance_exec(&condition_block)
+ end
+
+ def execute(context, opts, arg)
+ return if noop? || !available?(opts)
+
+ if arg.present?
+ context.instance_exec(arg, &action_block)
+ elsif action_block.arity == 0
+ context.instance_exec(&action_block)
+ end
+ end
+
+ def to_h(opts)
+ desc = description
+ if desc.respond_to?(:call)
+ context = OpenStruct.new(opts)
+ desc = context.instance_exec(&desc) rescue ''
+ end
+
+ {
+ name: name,
+ aliases: aliases,
+ description: desc,
+ params: params
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/dsl.rb b/lib/gitlab/slash_commands/dsl.rb
new file mode 100644
index 00000000000..50b0937d267
--- /dev/null
+++ b/lib/gitlab/slash_commands/dsl.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module SlashCommands
+ module Dsl
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :command_definitions, instance_accessor: false do
+ []
+ end
+
+ cattr_accessor :command_definitions_by_name, instance_accessor: false do
+ {}
+ end
+ end
+
+ class_methods do
+ # Allows to give a description to the next slash command.
+ # This description is shown in the autocomplete menu.
+ # It accepts a block that will be evaluated with the context given to
+ # `CommandDefintion#to_h`.
+ #
+ # Example:
+ #
+ # desc do
+ # "This is a dynamic description for #{noteable.to_ability_name}"
+ # end
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def desc(text = '', &block)
+ @description = block_given? ? block : text
+ end
+
+ # Allows to define params for the next slash command.
+ # These params are shown in the autocomplete menu.
+ #
+ # Example:
+ #
+ # params "~label ~label2"
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def params(*params)
+ @params = params
+ end
+
+ # Allows to define conditions that must be met in order for the command
+ # to be returned by `.command_names` & `.command_definitions`.
+ # It accepts a block that will be evaluated with the context given to
+ # `CommandDefintion#to_h`.
+ #
+ # Example:
+ #
+ # condition do
+ # project.public?
+ # end
+ # command :command_key do |arguments|
+ # # Awesome code block
+ # end
+ def condition(&block)
+ @condition_block = block
+ end
+
+ # Registers a new command which is recognizeable from body of email or
+ # comment.
+ # It accepts aliases and takes a block.
+ #
+ # Example:
+ #
+ # command :my_command, :alias_for_my_command do |arguments|
+ # # Awesome code block
+ # end
+ def command(*command_names, &block)
+ name, *aliases = command_names
+
+ definition = CommandDefinition.new(
+ name,
+ aliases: aliases,
+ description: @description,
+ params: @params,
+ condition_block: @condition_block,
+ action_block: block
+ )
+
+ self.command_definitions << definition
+
+ definition.all_names.each do |name|
+ self.command_definitions_by_name[name] = definition
+ end
+
+ @description = nil
+ @params = nil
+ @condition_block = nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/extractor.rb b/lib/gitlab/slash_commands/extractor.rb
new file mode 100644
index 00000000000..a672e5e4855
--- /dev/null
+++ b/lib/gitlab/slash_commands/extractor.rb
@@ -0,0 +1,122 @@
+module Gitlab
+ module SlashCommands
+ # This class takes an array of commands that should be extracted from a
+ # given text.
+ #
+ # ```
+ # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+ # ```
+ class Extractor
+ attr_reader :command_definitions
+
+ def initialize(command_definitions)
+ @command_definitions = command_definitions
+ end
+
+ # Extracts commands from content and return an array of commands.
+ # The array looks like the following:
+ # [
+ # ['command1'],
+ # ['command3', 'arg1 arg2'],
+ # ]
+ # The command and the arguments are stripped.
+ # The original command text is removed from the given `content`.
+ #
+ # Usage:
+ # ```
+ # extractor = Gitlab::SlashCommands::Extractor.new([:open, :assign, :labels])
+ # msg = %(hello\n/labels ~foo ~"bar baz"\nworld)
+ # commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
+ # msg #=> "hello\nworld"
+ # ```
+ def extract_commands(content, opts = {})
+ return [content, []] unless content
+
+ content = content.dup
+
+ commands = []
+
+ content.delete!("\r")
+ content.gsub!(commands_regex(opts)) do
+ if $~[:cmd]
+ commands << [$~[:cmd], $~[:arg]].reject(&:blank?)
+ ''
+ else
+ $~[0]
+ end
+ end
+
+ [content.strip, commands]
+ end
+
+ private
+
+ # Builds a regular expression to match known commands.
+ # First match group captures the command name and
+ # second match group captures its arguments.
+ #
+ # It looks something like:
+ #
+ # /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
+ def commands_regex(opts)
+ names = command_names(opts).map(&:to_s)
+
+ @commands_regex ||= %r{
+ (?<code>
+ # Code blocks:
+ # ```
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # ```
+
+ ^```
+ .+?
+ \n```$
+ )
+ |
+ (?<html>
+ # HTML block:
+ # <tag>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # </tag>
+
+ ^<[^>]+?>\n
+ .+?
+ \n<\/[^>]+?>$
+ )
+ |
+ (?<html>
+ # Quote block:
+ # >>>
+ # Anything, including `/cmd arg` which are ignored by this filter
+ # >>>
+
+ ^>>>
+ .+?
+ \n>>>$
+ )
+ |
+ (?:
+ # Command not in a blockquote, blockcode, or HTML tag:
+ # /close
+
+ ^\/
+ (?<cmd>#{Regexp.union(names)})
+ (?:
+ [ ]
+ (?<arg>[^\/\n]*)
+ )?
+ (?:\n|$)
+ )
+ }mx
+ end
+
+ def command_names(opts)
+ command_definitions.flat_map do |command|
+ next if command.noop?
+
+ command.all_names
+ end.compact
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
index 760ff3e614a..7ebec8e2cff 100644
--- a/lib/gitlab/template/base_template.rb
+++ b/lib/gitlab/template/base_template.rb
@@ -1,8 +1,9 @@
module Gitlab
module Template
class BaseTemplate
- def initialize(path)
+ def initialize(path, project = nil)
@path = path
+ @finder = self.class.finder(project)
end
def name
@@ -10,23 +11,32 @@ module Gitlab
end
def content
- File.read(@path)
+ @finder.read(@path)
+ end
+
+ def to_json
+ { name: name, content: content }
end
class << self
- def all
- self.categories.keys.flat_map { |cat| by_category(cat) }
+ def all(project = nil)
+ if categories.any?
+ categories.keys.flat_map { |cat| by_category(cat, project) }
+ else
+ by_category("", project)
+ end
end
- def find(key)
- file_name = "#{key}#{self.extension}"
-
- directory = select_directory(file_name)
- directory ? new(File.join(category_directory(directory), file_name)) : nil
+ def find(key, project = nil)
+ path = self.finder(project).find(key)
+ path.present? ? new(path, project) : nil
end
+ # Set categories as sub directories
+ # Example: { "category_name_1" => "directory_path_1", "category_name_2" => "directory_name_2" }
+ # Default is no category with all files in base dir of each class
def categories
- raise NotImplementedError
+ {}
end
def extension
@@ -37,29 +47,40 @@ module Gitlab
raise NotImplementedError
end
- def by_category(category)
- templates_for_directory(category_directory(category))
+ # Defines which strategy will be used to get templates files
+ # RepoTemplateFinder - Finds templates on project repository, templates are filtered perproject
+ # GlobalTemplateFinder - Finds templates on gitlab installation source, templates can be used in all projects
+ def finder(project = nil)
+ raise NotImplementedError
end
- def category_directory(category)
- File.join(base_dir, categories[category])
+ def by_category(category, project = nil)
+ directory = category_directory(category)
+ files = finder(project).list_files_for(directory)
+
+ files.map { |f| new(f, project) }
end
- private
+ def category_directory(category)
+ return base_dir unless category.present?
- def select_directory(file_name)
- categories.keys.find do |category|
- File.exist?(File.join(category_directory(category), file_name))
- end
+ File.join(base_dir, categories[category])
end
- def templates_for_directory(dir)
- dir << '/' unless dir.end_with?('/')
- Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
- end
+ # If template is organized by category it returns { category_name: [{ name: template_name }, { name: template2_name }] }
+ # If no category is present returns [{ name: template_name }, { name: template2_name}]
+ def dropdown_names(project = nil)
+ return [] if project && !project.repository.exists?
- def filter_regex
- @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ if categories.any?
+ categories.keys.map do |category|
+ files = self.by_category(category, project)
+ [category, files.map { |t| { name: t.name } }]
+ end.to_h
+ else
+ files = self.all(project)
+ files.map { |t| { name: t.name } }
+ end
end
end
end
diff --git a/lib/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb
new file mode 100644
index 00000000000..473b05257c6
--- /dev/null
+++ b/lib/gitlab/template/finders/base_template_finder.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Template
+ module Finders
+ class BaseTemplateFinder
+ def initialize(base_dir)
+ @base_dir = base_dir
+ end
+
+ def list_files_for
+ raise NotImplementedError
+ end
+
+ def read
+ raise NotImplementedError
+ end
+
+ def find
+ raise NotImplementedError
+ end
+
+ def category_directory(category)
+ return @base_dir unless category.present?
+
+ @base_dir + @categories[category]
+ end
+
+ class << self
+ def filter_regex(extension)
+ /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/global_template_finder.rb b/lib/gitlab/template/finders/global_template_finder.rb
new file mode 100644
index 00000000000..831da45191f
--- /dev/null
+++ b/lib/gitlab/template/finders/global_template_finder.rb
@@ -0,0 +1,38 @@
+# Searches and reads file present on Gitlab installation directory
+module Gitlab
+ module Template
+ module Finders
+ class GlobalTemplateFinder < BaseTemplateFinder
+ def initialize(base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ super(base_dir)
+ end
+
+ def read(path)
+ File.read(path)
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+
+ directory = select_directory(file_name)
+ directory ? File.join(category_directory(directory), file_name) : nil
+ end
+
+ def list_files_for(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{@extension}")).select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ @categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
new file mode 100644
index 00000000000..22c39436cb2
--- /dev/null
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -0,0 +1,59 @@
+# Searches and reads files present on each Gitlab project repository
+module Gitlab
+ module Template
+ module Finders
+ class RepoTemplateFinder < BaseTemplateFinder
+ # Raised when file is not found
+ class FileNotFoundError < StandardError; end
+
+ def initialize(project, base_dir, extension, categories = {})
+ @categories = categories
+ @extension = extension
+ @repository = project.repository
+ @commit = @repository.head_commit if @repository.exists?
+
+ super(base_dir)
+ end
+
+ def read(path)
+ blob = @repository.blob_at(@commit.id, path) if @commit
+ raise FileNotFoundError if blob.nil?
+ blob.data
+ end
+
+ def find(key)
+ file_name = "#{key}#{@extension}"
+ directory = select_directory(file_name)
+ raise FileNotFoundError if directory.nil?
+
+ category_directory(directory) + file_name
+ end
+
+ def list_files_for(dir)
+ return [] unless @commit
+
+ dir << '/' unless dir.end_with?('/')
+
+ entries = @repository.tree(:head, dir).entries
+
+ names = entries.map(&:name)
+ names.select { |f| f =~ self.class.filter_regex(@extension) }
+ end
+
+ private
+
+ def select_directory(file_name)
+ return [] unless @commit
+
+ # Insert root as directory
+ directories = ["", @categories.keys]
+
+ directories.find do |category|
+ path = category_directory(category) + file_name
+ @repository.blob_at(@commit.id, path)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb
deleted file mode 100644
index 964fbfd4de3..00000000000
--- a/lib/gitlab/template/gitignore.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Gitlab
- module Template
- class Gitignore < BaseTemplate
- class << self
- def extension
- '.gitignore'
- end
-
- def categories
- {
- "Languages" => '',
- "Global" => 'Global'
- }
- end
-
- def base_dir
- Rails.root.join('vendor/gitignore')
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/template/gitignore_template.rb b/lib/gitlab/template/gitignore_template.rb
new file mode 100644
index 00000000000..8d2a9d2305c
--- /dev/null
+++ b/lib/gitlab/template/gitignore_template.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Template
+ class GitignoreTemplate < BaseTemplate
+ class << self
+ def extension
+ '.gitignore'
+ end
+
+ def categories
+ {
+ "Languages" => '',
+ "Global" => 'Global'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitignore')
+ end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb
deleted file mode 100644
index 7f480fe33c0..00000000000
--- a/lib/gitlab/template/gitlab_ci_yml.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module Template
- class GitlabCiYml < BaseTemplate
- def content
- explanation = "# This file is a template, and might need editing before it works on your project."
- [explanation, super].join("\n")
- end
-
- class << self
- def extension
- '.gitlab-ci.yml'
- end
-
- def categories
- {
- "General" => '',
- "Pages" => 'Pages'
- }
- end
-
- def base_dir
- Rails.root.join('vendor/gitlab-ci-yml')
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/template/gitlab_ci_yml_template.rb b/lib/gitlab/template/gitlab_ci_yml_template.rb
new file mode 100644
index 00000000000..8d1a1ed54c9
--- /dev/null
+++ b/lib/gitlab/template/gitlab_ci_yml_template.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Template
+ class GitlabCiYmlTemplate < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ '.gitlab-ci.yml'
+ end
+
+ def categories
+ {
+ "General" => '',
+ "Pages" => 'Pages'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitlab-ci-yml')
+ end
+
+ def finder(project = nil)
+ Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/issue_template.rb b/lib/gitlab/template/issue_template.rb
new file mode 100644
index 00000000000..c6fa8d3eafc
--- /dev/null
+++ b/lib/gitlab/template/issue_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class IssueTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/issue_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/merge_request_template.rb b/lib/gitlab/template/merge_request_template.rb
new file mode 100644
index 00000000000..f826c02f3b5
--- /dev/null
+++ b/lib/gitlab/template/merge_request_template.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Template
+ class MergeRequestTemplate < BaseTemplate
+ class << self
+ def extension
+ '.md'
+ end
+
+ def base_dir
+ '.gitlab/merge_request_templates/'
+ end
+
+ def finder(project)
+ Gitlab::Template::Finders::RepoTemplateFinder.new(project, self.base_dir, self.extension, self.categories)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
index 83f91de810c..d4020af76f9 100644
--- a/lib/gitlab/themes.rb
+++ b/lib/gitlab/themes.rb
@@ -2,6 +2,8 @@ module Gitlab
# Module containing GitLab's application theme definitions and helper methods
# for accessing them.
module Themes
+ extend self
+
# Theme ID used when no `default_theme` configuration setting is provided.
APPLICATION_DEFAULT = 2
@@ -22,7 +24,7 @@ module Gitlab
# classes that might be applied to the `body` element
#
# Returns a String
- def self.body_classes
+ def body_classes
THEMES.collect(&:css_class).uniq.join(' ')
end
@@ -33,26 +35,26 @@ module Gitlab
# id - Integer ID
#
# Returns a Theme
- def self.by_id(id)
+ def by_id(id)
THEMES.detect { |t| t.id == id } || default
end
# Returns the number of defined Themes
- def self.count
+ def count
THEMES.size
end
# Get the default Theme
#
# Returns a Theme
- def self.default
+ def default
by_id(default_id)
end
# Iterate through each Theme
#
# Yields the Theme object
- def self.each(&block)
+ def each(&block)
THEMES.each(&block)
end
@@ -61,7 +63,7 @@ module Gitlab
# user - User record
#
# Returns a Theme
- def self.for_user(user)
+ def for_user(user)
if user
by_id(user.theme_id)
else
@@ -71,7 +73,7 @@ module Gitlab
private
- def self.default_id
+ def default_id
id = Gitlab.config.gitlab.default_theme.to_i
# Prevent an invalid configuration setting from causing an infinite loop
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index fe65c246101..99d0c28e749 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -22,6 +22,8 @@ module Gitlab
note_url
when WikiPage
wiki_page_url
+ when ProjectSnippet
+ project_snippet_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index d1b42c1f9b9..9858d2e7d83 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -1,7 +1,23 @@
module Gitlab
- module UserAccess
- def self.allowed?(user)
- return false if user.blocked?
+ class UserAccess
+ attr_reader :user, :project
+
+ def initialize(user, project: nil)
+ @user = user
+ @project = project
+ end
+
+ def can_do_action?(action)
+ @permission_cache ||= {}
+ @permission_cache[action] ||= user.can?(action, project)
+ end
+
+ def cannot_do_action?(action)
+ !can_do_action?(action)
+ end
+
+ def allowed?
+ return false if user.blank? || user.blocked?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
return false unless Gitlab::LDAP::Access.allowed?(user)
@@ -9,5 +25,35 @@ module Gitlab
true
end
+
+ def can_push_to_branch?(ref)
+ return false unless user
+
+ if project.protected_branch?(ref)
+ return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
+
+ access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
+ access_levels.any? { |access_level| access_level.check_access(user) }
+ else
+ user.can?(:push_code, project)
+ end
+ end
+
+ def can_merge_to_branch?(ref)
+ return false unless user
+
+ if project.protected_branch?(ref)
+ access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
+ access_levels.any? { |access_level| access_level.check_access(user) }
+ else
+ user.can?(:push_code, project)
+ end
+ end
+
+ def can_read_project?
+ return false unless user
+
+ user.can?(:read_project, project)
+ end
end
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9..e59ead5d76c 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,7 +7,7 @@ module Gitlab
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
- Popen::popen(cmd).last.zero?
+ Popen.popen(cmd).last.zero?
end
def force_utf8(str)
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index bc0193a6c32..c6826a09bd2 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -4,6 +4,7 @@ require 'json'
module Gitlab
class Workhorse
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
+ VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
class << self
def git_http_ok(repository, user)
@@ -63,6 +64,23 @@ module Gitlab
]
end
+ def send_artifacts_entry(build, entry)
+ params = {
+ 'Archive' => build.artifacts_file.path,
+ 'Entry' => Base64.encode64(entry.path)
+ }
+
+ [
+ SEND_DATA_HEADER,
+ "artifacts-entry:#{encode(params)}"
+ ]
+ end
+
+ def version
+ path = Rails.root.join(VERSION_FILE)
+ path.readable? ? path.read.chomp : 'unknown'
+ end
+
protected
def encode(hash)
diff --git a/lib/repository_cache.rb b/lib/repository_cache.rb
index 8ddc3511293..068a95790c0 100644
--- a/lib/repository_cache.rb
+++ b/lib/repository_cache.rb
@@ -1,14 +1,15 @@
# Interface to the Redis-backed cache store used by the Repository model
class RepositoryCache
- attr_reader :namespace, :backend
+ attr_reader :namespace, :backend, :project_id
- def initialize(namespace, backend = Rails.cache)
+ def initialize(namespace, project_id, backend = Rails.cache)
@namespace = namespace
@backend = backend
+ @project_id = project_id
end
def cache_key(type)
- "#{type}:#{namespace}"
+ "#{type}:#{namespace}:#{project_id}"
end
def expire(key)
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 3358ed6773e..4edfd015074 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -1,171 +1,27 @@
-require 'cgi'
-
module Rouge
module Formatters
- class HTMLGitlab < Rouge::Formatter
+ class HTMLGitlab < Rouge::Formatters::HTML
tag 'html_gitlab'
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
- # [+nowrap+] If set to True, don't wrap the output at all, not
- # even inside a <tt><pre></tt> tag (default: false).
- # [+cssclass+] CSS class for the wrapping <tt><div></tt> tag
- # (default: 'highlight').
- # [+linenos+] If set to 'table', output line numbers as a table
- # with two cells, one containing the line numbers,
- # the other the whole code. This is copy paste friendly,
- # but may cause alignment problems with some browsers
- # or fonts. If set to 'inline', the line numbers will
- # be integrated in the <tt><pre></tt> tag that contains
- # the code (default: nil).
# [+linenostart+] The line number for the first line (default: 1).
- # [+lineanchors+] If set to true the formatter will wrap each output
- # line in an anchor tag with a name of L-linenumber.
- # This allows easy linking to certain lines
- # (default: false).
- # [+lineanchorsid+] If lineanchors is true the name of the anchors can
- # be changed with lineanchorsid to e.g. foo-linenumber
- # (default: 'L').
- # [+anchorlinenos+] If set to true, will wrap line numbers in <tt><a></tt>
- # tags. Used in combination with linenos and lineanchors
- # (default: false).
- # [+inline_theme+] Inline CSS styles for the <pre> tag (default: false).
- def initialize(
- nowrap: false,
- cssclass: 'highlight',
- linenos: nil,
- linenostart: 1,
- lineanchors: false,
- lineanchorsid: 'L',
- anchorlinenos: false,
- inline_theme: nil
- )
- @nowrap = nowrap
- @cssclass = cssclass
- @linenos = linenos
+ def initialize(linenostart: 1)
@linenostart = linenostart
- @lineanchors = lineanchors
- @lineanchorsid = lineanchorsid
- @anchorlinenos = anchorlinenos
- @inline_theme = Theme.find(inline_theme).new if inline_theme.is_a?(String)
- end
-
- def render(tokens)
- case @linenos
- when 'table'
- render_tableized(tokens)
- when 'inline'
- render_untableized(tokens)
- else
- render_untableized(tokens)
- end
- end
-
- alias_method :format, :render
-
- private
-
- def render_untableized(tokens)
- data = process_tokens(tokens)
-
- html = ''
- html << "<pre class=\"#{@cssclass}\"><code>" unless @nowrap
- html << wrap_lines(data[:code])
- html << "</code></pre>\n" unless @nowrap
- html
+ @line_number = linenostart
end
- def render_tableized(tokens)
- data = process_tokens(tokens)
-
- html = ''
- html << "<div class=\"#{@cssclass}\">" unless @nowrap
- html << '<table><tbody>'
- html << "<td class=\"linenos\"><pre>"
- html << wrap_linenos(data[:numbers])
- html << '</pre></td>'
- html << "<td class=\"lines\"><pre><code>"
- html << wrap_lines(data[:code])
- html << '</code></pre></td>'
- html << '</tbody></table>'
- html << '</div>' unless @nowrap
- html
- end
-
- def process_tokens(tokens)
- rendered = []
- current_line = ''
-
- tokens.each do |tok, val|
- # In the case of multi-line values (e.g. comments), we need to apply
- # styling to each line since span elements are inline.
- val.lines.each do |line|
- stripped = line.chomp
- current_line << span(tok, stripped)
-
- if line.end_with?("\n")
- rendered << current_line
- current_line = ''
- end
- end
- end
-
- # Add leftover text
- rendered << current_line if current_line.present?
-
- num_lines = rendered.size
- numbers = (@linenostart..num_lines + @linenostart - 1).to_a
-
- { numbers: numbers, code: rendered }
- end
-
- def wrap_linenos(numbers)
- if @anchorlinenos
- numbers.map! do |number|
- "<a href=\"##{@lineanchorsid}#{number}\">#{number}</a>"
- end
- end
- numbers.join("\n")
- end
-
- def wrap_lines(lines)
- if @lineanchors
- lines = lines.each_with_index.map do |line, index|
- number = index + @linenostart
-
- if @linenos == 'inline'
- "<a name=\"L#{number}\"></a>" \
- "<span class=\"linenos\">#{number}</span>" \
- "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \
- '</span>'
- else
- "<span id=\"#{@lineanchorsid}#{number}\" class=\"line\">#{line}" \
- '</span>'
- end
- end
- elsif @linenos == 'inline'
- lines = lines.each_with_index.map do |line, index|
- number = index + @linenostart
- "<span class=\"linenos\">#{number}</span>#{line}"
- end
- end
-
- lines.join("\n")
- end
+ def stream(tokens, &b)
+ is_first = true
+ token_lines(tokens) do |line|
+ yield "\n" unless is_first
+ is_first = false
- def span(tok, val)
- # http://stackoverflow.com/a/1600584/2587286
- val = CGI.escapeHTML(val)
+ yield %(<span id="LC#{@line_number}" class="line">)
+ line.each { |token, value| yield span(token, value.chomp) }
+ yield %(</span>)
- if tok.shortname.empty?
- val
- else
- if @inline_theme
- rules = @inline_theme.style_for(tok).rendered_rules
- "<span style=\"#{rules.to_a.join(';')}\"#{val}</span>"
- else
- "<span class=\"#{tok.shortname}\">#{val}</span>"
- end
+ @line_number += 1
end
end
end
diff --git a/lib/tasks/downtime_check.rake b/lib/tasks/downtime_check.rake
new file mode 100644
index 00000000000..afe5d42910c
--- /dev/null
+++ b/lib/tasks/downtime_check.rake
@@ -0,0 +1,12 @@
+desc 'Checks if migrations in a branch require downtime'
+task downtime_check: :environment do
+ if defined?(Gitlab::License)
+ repo = 'gitlab-ee'
+ else
+ repo = 'gitlab-ce'
+ end
+
+ `git fetch https://gitlab.com/gitlab-org/#{repo}.git --depth 1`
+
+ Rake::Task['gitlab:db:downtime_check'].invoke('FETCH_HEAD')
+end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index e930ace1041..993112aee3b 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -4,7 +4,7 @@ namespace :gemojione do
require 'digest/sha2'
require 'json'
- dir = Gemojione.index.images_path
+ dir = Gemojione.images_path
digests = []
aliases = Hash.new { |hash, key| hash[key] = [] }
aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
@@ -50,9 +50,14 @@ namespace :gemojione do
SIZE = 20
RETINA = SIZE * 2
+ # Update these values to the width and height of the spritesheet when
+ # new emoji are added.
+ SPRITESHEET_WIDTH = 860
+ SPRITESHEET_HEIGHT = 840
+
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
+ FileUtils.cp_r(Gemojione.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
@@ -64,7 +69,7 @@ namespace :gemojione do
# Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')"
- SpriteFactory.run!(File.join(tmpdir, 'images'), {
+ SpriteFactory.run!(File.join(tmpdir, 'png'), {
output_style: style_path,
output_image: "app/assets/images/emoji.png",
selector: '.emoji-',
@@ -97,7 +102,7 @@ namespace :gemojione do
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
background-image: image-url('emoji@2x.png');
- background-size: 840px 820px;
+ background-size: #{SPRITESHEET_WIDTH}px #{SPRITESHEET_HEIGHT}px;
}
}
CSS
@@ -107,7 +112,7 @@ namespace :gemojione do
# Now do it again but for Retina
Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing
- FileUtils.cp_r(Gemojione.index.images_path, tmpdir)
+ FileUtils.cp_r(Gemojione.images_path, tmpdir)
Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png|
@@ -116,7 +121,7 @@ namespace :gemojione do
end
# Combine the resized assets into a packed sprite and re-generate the SCSS
- SpriteFactory.run!(File.join(tmpdir, 'images'), {
+ SpriteFactory.run!(File.join(tmpdir), {
output_image: "app/assets/images/emoji@2x.png",
style: false,
nocomments: true,
diff --git a/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake
index 5dbf7d61e06..83dd870fa31 100644
--- a/lib/tasks/gitlab/bulk_add_permission.rake
+++ b/lib/tasks/gitlab/bulk_add_permission.rake
@@ -4,13 +4,13 @@ namespace :gitlab do
task all_users_to_all_projects: :environment do |t, args|
user_ids = User.where(admin: false).pluck(:id)
admin_ids = User.where(admin: true).pluck(:id)
- projects_ids = Project.pluck(:id)
+ project_ids = Project.pluck(:id)
- puts "Importing #{user_ids.size} users into #{projects_ids.size} projects"
- ProjectMember.add_users_into_projects(projects_ids, user_ids, ProjectMember::DEVELOPER)
+ puts "Importing #{user_ids.size} users into #{project_ids.size} projects"
+ ProjectMember.add_users_to_projects(project_ids, user_ids, ProjectMember::DEVELOPER)
- puts "Importing #{admin_ids.size} admins into #{projects_ids.size} projects"
- ProjectMember.add_users_into_projects(projects_ids, admin_ids, ProjectMember::MASTER)
+ puts "Importing #{admin_ids.size} admins into #{project_ids.size} projects"
+ ProjectMember.add_users_to_projects(project_ids, admin_ids, ProjectMember::MASTER)
end
desc "GitLab | Add a specific user to all projects (as a developer)"
@@ -18,7 +18,7 @@ namespace :gitlab do
user = User.find_by(email: args.email)
project_ids = Project.pluck(:id)
puts "Importing #{user.email} users into #{project_ids.size} projects"
- ProjectMember.add_users_into_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER)
+ ProjectMember.add_users_to_projects(project_ids, Array.wrap(user.id), ProjectMember::DEVELOPER)
end
desc "GitLab | Add all users to all groups (admin users are added as owners)"
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index e9a4e37ec48..5f4a6bbfa35 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -46,7 +46,7 @@ namespace :gitlab do
}
correct_options = options.map do |name, value|
- run(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
+ run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
end
if correct_options.all?
@@ -64,7 +64,7 @@ namespace :gitlab do
for_more_information(
see_installation_guide_section "GitLab"
)
- end
+ end
end
end
@@ -73,7 +73,7 @@ namespace :gitlab do
database_config_file = Rails.root.join("config", "database.yml")
- if File.exists?(database_config_file)
+ if File.exist?(database_config_file)
puts "yes".color(:green)
else
puts "no".color(:red)
@@ -94,7 +94,7 @@ namespace :gitlab do
gitlab_config_file = Rails.root.join("config", "gitlab.yml")
- if File.exists?(gitlab_config_file)
+ if File.exist?(gitlab_config_file)
puts "yes".color(:green)
else
puts "no".color(:red)
@@ -113,7 +113,7 @@ namespace :gitlab do
print "GitLab config outdated? ... "
gitlab_config_file = Rails.root.join("config", "gitlab.yml")
- unless File.exists?(gitlab_config_file)
+ unless File.exist?(gitlab_config_file)
puts "can't check because of previous errors".color(:magenta)
end
@@ -144,7 +144,7 @@ namespace :gitlab do
script_path = "/etc/init.d/gitlab"
- if File.exists?(script_path)
+ if File.exist?(script_path)
puts "yes".color(:green)
else
puts "no".color(:red)
@@ -169,7 +169,7 @@ namespace :gitlab do
recipe_path = Rails.root.join("lib/support/init.d/", "gitlab")
script_path = "/etc/init.d/gitlab"
- unless File.exists?(script_path)
+ unless File.exist?(script_path)
puts "can't check because of previous errors".color(:magenta)
return
end
@@ -316,7 +316,7 @@ namespace :gitlab do
min_redis_version = "2.8.0"
print "Redis version >= #{min_redis_version}? ... "
- redis_version = run(%W(redis-cli --version))
+ redis_version = run_command(%W(redis-cli --version))
redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
if redis_version &&
(Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
@@ -361,7 +361,7 @@ namespace :gitlab do
Gitlab.config.repositories.storages.each do |name, repo_base_path|
print "#{name}... "
- if File.exists?(repo_base_path)
+ if File.exist?(repo_base_path)
puts "yes".color(:green)
else
puts "no".color(:red)
@@ -385,7 +385,7 @@ namespace :gitlab do
Gitlab.config.repositories.storages.each do |name, repo_base_path|
print "#{name}... "
- unless File.exists?(repo_base_path)
+ unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
return
end
@@ -408,7 +408,7 @@ namespace :gitlab do
Gitlab.config.repositories.storages.each do |name, repo_base_path|
print "#{name}... "
- unless File.exists?(repo_base_path)
+ unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
return
end
@@ -438,7 +438,7 @@ namespace :gitlab do
Gitlab.config.repositories.storages.each do |name, repo_base_path|
print "#{name}... "
- unless File.exists?(repo_base_path)
+ unless File.exist?(repo_base_path)
puts "can't check because of previous errors".color(:magenta)
return
end
@@ -784,7 +784,7 @@ namespace :gitlab do
servers.each do |server|
puts "Server: #{server}"
Gitlab::LDAP::Adapter.open(server) do |adapter|
- users = adapter.users(adapter.config.uid, '*', 100)
+ users = adapter.users(adapter.config.uid, '*', limit)
users.each do |user|
puts "\tDN: #{user.dn}\t #{adapter.config.uid}: #{user.uid}"
end
@@ -893,7 +893,7 @@ namespace :gitlab do
def check_ruby_version
required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version)))
+ current_version = Gitlab::VersionInfo.parse(run_command(%W(ruby --version)))
print "Ruby version >= #{required_version} ? ... "
@@ -910,7 +910,7 @@ namespace :gitlab do
def check_git_version
required_version = Gitlab::VersionInfo.new(2, 7, 3)
- current_version = Gitlab::VersionInfo.parse(run(%W(#{Gitlab.config.git.bin_path} --version)))
+ current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
print "Git version >= #{required_version} ? ... "
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 7230b9485be..7c96bc864ce 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -25,6 +25,10 @@ namespace :gitlab do
desc 'Drop all tables'
task :drop_tables => :environment do
connection = ActiveRecord::Base.connection
+
+ # If MySQL, turn off foreign key checks
+ connection.execute('SET FOREIGN_KEY_CHECKS=0') if Gitlab::Database.mysql?
+
tables = connection.tables
tables.delete 'schema_migrations'
# Truncate schema_migrations to ensure migrations re-run
@@ -35,6 +39,9 @@ namespace :gitlab do
# MySQL: http://dev.mysql.com/doc/refman/5.7/en/drop-table.html
# Add `IF EXISTS` because cascade could have already deleted a table.
tables.each { |t| connection.execute("DROP TABLE IF EXISTS #{connection.quote_table_name(t)} CASCADE") }
+
+ # If MySQL, re-enable foreign key checks
+ connection.execute('SET FOREIGN_KEY_CHECKS=1') if Gitlab::Database.mysql?
end
desc 'Configures the database by running migrate, or by loading the schema and seeding if needed'
@@ -46,5 +53,20 @@ namespace :gitlab do
Rake::Task['db:seed_fu'].invoke
end
end
+
+ desc 'Checks if migrations require downtime or not'
+ task :downtime_check, [:ref] => :environment do |_, args|
+ abort 'You must specify a Git reference to compare with' unless args[:ref]
+
+ require 'shellwords'
+
+ ref = Shellwords.escape(args[:ref])
+
+ migrations = `git diff #{ref}.. --name-only -- db/migrate`.lines.
+ map { |file| Rails.root.join(file.strip).to_s }.
+ select { |file| File.file?(file) }
+
+ Gitlab::DowntimeCheck.new.check_and_print(migrations)
+ end
end
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index fe43d40e6d2..dffea8ed155 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
# check Ruby version
ruby_version = run_and_match(%W(ruby --version), /[\d\.p]+/).try(:to_s)
# check Gem version
- gem_version = run(%W(gem --version))
+ gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
# check Bundler version
@@ -17,7 +17,7 @@ namespace :gitlab do
puts ""
puts "System information".color(:yellow)
puts "System:\t\t#{os_name || "unknown".color(:red)}"
- puts "Current User:\t#{run(%W(whoami))}"
+ puts "Current User:\t#{run_command(%W(whoami))}"
puts "Using RVM:\t#{rvm_version.present? ? "yes".color(:green) : "no"}"
puts "RVM Version:\t#{rvm_version}" if rvm_version.present?
puts "Ruby Version:\t#{ruby_version || "unknown".color(:red)}"
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index c85ebdf8619..bb7eb852f1b 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -5,7 +5,8 @@ namespace :gitlab do
warn_user_is_not_gitlab
default_version = Gitlab::Shell.version_required
- args.with_defaults(tag: 'v' + default_version, 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
@@ -15,7 +16,12 @@ namespace :gitlab do
target_dir = Gitlab.config.gitlab_shell.path
# Clone if needed
- unless File.directory?(target_dir)
+ 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
@@ -84,7 +90,7 @@ namespace :gitlab do
task build_missing_projects: :environment do
Project.find_each(batch_size: 1000) do |project|
path_to_repo = project.repository.path_to_repo
- if File.exists?(path_to_repo)
+ if File.exist?(path_to_repo)
print '-'
else
if Gitlab::Shell.new.add_repository(project.repository_storage_path,
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index ab96b1d3593..74be413423a 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -23,7 +23,7 @@ namespace :gitlab do
# 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(%W(lsb_release -irs))
+ os_name = run_command(%W(lsb_release -irs))
os_name ||= if File.readable?('/etc/system-release')
File.read('/etc/system-release')
end
@@ -34,7 +34,7 @@ namespace :gitlab do
os_name ||= if File.readable?('/etc/SuSE-release')
File.read('/etc/SuSE-release')
end
- os_name ||= if os_x_version = run(%W(sw_vers -productVersion))
+ 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')
@@ -62,10 +62,10 @@ namespace :gitlab do
# Returns nil if nothing matched
# Returns the MatchData if the pattern matched
#
- # see also #run
+ # see also #run_command
# see also String#match
def run_and_match(command, regexp)
- run(command).try(:match, regexp)
+ run_command(command).try(:match, regexp)
end
# Runs the given command
@@ -74,7 +74,7 @@ namespace :gitlab do
# Returns the output of the command otherwise
#
# see also #run_and_match
- def run(command)
+ def run_command(command)
output, _ = Gitlab::Popen.popen(command)
output
rescue Errno::ENOENT
@@ -82,7 +82,7 @@ namespace :gitlab do
end
def uid_for(user_name)
- run(%W(id -u #{user_name})).chomp.to_i
+ run_command(%W(id -u #{user_name})).chomp.to_i
end
def gid_for(group_name)
@@ -96,7 +96,7 @@ namespace :gitlab do
def warn_user_is_not_gitlab
unless @warned_user_not_gitlab
gitlab_user = Gitlab.config.gitlab.user
- current_user = run(%W(whoami)).chomp
+ 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."
diff --git a/lib/tasks/gitlab/track_deployment.rake b/lib/tasks/gitlab/track_deployment.rake
new file mode 100644
index 00000000000..84aa2e8507a
--- /dev/null
+++ b/lib/tasks/gitlab/track_deployment.rake
@@ -0,0 +1,9 @@
+namespace :gitlab do
+ desc 'GitLab | Tracks a deployment in GitLab Performance Monitoring'
+ task track_deployment: :environment do
+ metric = Gitlab::Metrics::Metric.
+ new('deployments', version: Gitlab::VERSION)
+
+ Gitlab::Metrics.submit_metrics([metric.to_hash])
+ end
+end
diff --git a/lib/tasks/gitlab/web_hook.rake b/lib/tasks/gitlab/web_hook.rake
index f467cc0ee29..49530e7a372 100644
--- a/lib/tasks/gitlab/web_hook.rake
+++ b/lib/tasks/gitlab/web_hook.rake
@@ -26,10 +26,10 @@ namespace :gitlab do
namespace_path = ENV['NAMESPACE']
projects = find_projects(namespace_path)
- projects_ids = projects.pluck(:id)
+ project_ids = projects.pluck(:id)
puts "Removing webhooks with the url '#{web_hook_url}' ... "
- count = WebHook.where(url: web_hook_url, project_id: projects_ids, type: 'ProjectHook').delete_all
+ count = WebHook.where(url: web_hook_url, project_id: project_ids, type: 'ProjectHook').delete_all
puts "#{count} webhooks were removed."
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index da255f5464b..8dbfa7751dc 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -34,21 +34,19 @@ task :spinach do
run_spinach_tests(nil)
end
-def run_command(cmd)
+def run_system_command(cmd)
system({'RAILS_ENV' => 'test', 'force' => 'yes'}, *cmd)
end
def run_spinach_command(args)
- run_command(%w(spinach -r rerun) + args)
+ run_system_command(%w(spinach -r rerun) + args)
end
def run_spinach_tests(tags)
- #run_command(%w(rake gitlab:setup)) or raise('gitlab:setup failed!')
-
success = run_spinach_command(%W(--tags #{tags}))
3.times do |_|
break if success
- break unless File.exists?('tmp/spinach-rerun.txt')
+ break unless File.exist?('tmp/spinach-rerun.txt')
tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp)
puts ''
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index 21c0e5f1d41..d3dcbd2c29b 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -7,5 +7,5 @@ end
unless Rails.env.production?
desc "GitLab | Run all tests on CI with simplecov"
- task test_ci: [:rubocop, :brakeman, 'teaspoon', :spinach, :spec]
+ task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec]
end
diff --git a/public/404.html b/public/404.html
index 4862770cc2a..92b7f4da0b9 100644
--- a/public/404.html
+++ b/public/404.html
@@ -1,55 +1,65 @@
<!DOCTYPE html>
<html>
<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
<title>The page you're looking for could not be found (404)</title>
<style>
- body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
- }
+ 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;
- }
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
- h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
- }
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
- h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
- }
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
- hr {
- margin: 18px 0;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
- }
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
+
+ img {
+ max-width: 40vw;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
</style>
</head>
<body>
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
404
</h1>
- <h3>The page you're looking for could not be found.</h3>
- <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>
+ <div class="container">
+ <h3>The page you're looking for could not be found.</h3>
+ <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>
+ </div>
</body>
</html>
diff --git a/public/422.html b/public/422.html
index 055b0bde165..f625f8a33b7 100644
--- a/public/422.html
+++ b/public/422.html
@@ -1,55 +1,65 @@
<!DOCTYPE html>
<html>
<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
<title>The change you requested was rejected (422)</title>
<style>
body {
color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
- }
+ 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;
- }
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
- h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
- }
+ 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;
+ }
- h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
- }
+ img {
+ max-width: 40vw;
+ }
- hr {
- margin: 18px 0;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
- }
- </style>
+ .container {
+ margin: auto 20px;
+ }
+ </style>
</head>
<body>
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
422
</h1>
- <h3>The change you requested was rejected.</h3>
- <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>
+ <div class="container">
+ <h3>The change you requested was rejected.</h3>
+ <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>
+ </div>
</body>
</html>
diff --git a/public/500.html b/public/500.html
index 3d59d1392f5..d76c66ba92a 100644
--- a/public/500.html
+++ b/public/500.html
@@ -1,54 +1,65 @@
<!DOCTYPE html>
<html>
<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
<title>Something went wrong (500)</title>
<style>
- body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
- }
+ 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;
- }
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
- h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
- }
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
- h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
- }
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
- hr {
- margin: 18px 0;
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
border: 0;
border-top: 1px solid #EEE;
border-bottom: 1px solid white;
}
+
+ img {
+ max-width: 40vw;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
</style>
</head>
+
<body>
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
500
</h1>
- <h3>Whoops, something went wrong on our end.</h3>
- <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>
+ <div class="container">
+ <h3>Whoops, something went wrong on our end.</h3>
+ <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>
+ </div>
</body>
</html>
diff --git a/public/502.html b/public/502.html
index 67dfd8a2743..1a3c7efc769 100644
--- a/public/502.html
+++ b/public/502.html
@@ -1,14 +1,13 @@
<!DOCTYPE html>
<html>
<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
<title>GitLab is not responding (502)</title>
<style>
body {
color: #666;
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
margin: auto;
font-size: 14px;
}
@@ -34,21 +33,33 @@
}
hr {
- margin: 18px 0;
+ max-width: 800px;
+ margin: 18px auto;
border: 0;
border-top: 1px solid #EEE;
border-bottom: 1px solid white;
}
+
+ img {
+ max-width: 40vw;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
</style>
</head>
+
<body>
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
502
</h1>
- <h3>Whoops, GitLab is taking too much time to respond.</h3>
- <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>
+ <div class="container">
+ <h3>Whoops, GitLab is taking too much time to respond.</h3>
+ <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>
+ </div>
</body>
</html>
diff --git a/public/503.html b/public/503.html
index 6ab1185658d..c1c4e3ffdb8 100644
--- a/public/503.html
+++ b/public/503.html
@@ -1,14 +1,13 @@
<!DOCTYPE html>
<html>
<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
<title>GitLab is not responding (503)</title>
<style>
body {
color: #666;
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
margin: auto;
font-size: 14px;
}
@@ -34,21 +33,33 @@
}
hr {
- margin: 18px 0;
+ max-width: 800px;
+ margin: 18px auto;
border: 0;
border-top: 1px solid #EEE;
border-bottom: 1px solid white;
}
+
+ img {
+ max-width: 40vw;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
</style>
</head>
+
<body>
<h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo"/><br />
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
503
</h1>
- <h3>Whoops, GitLab is currently unavailable.</h3>
- <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>
+ <div class="container">
+ <h3>Whoops, GitLab is currently unavailable.</h3>
+ <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>
+ </div>
</body>
</html>
diff --git a/public/deploy.html b/public/deploy.html
index 48976dacf41..142472b6c35 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -1,54 +1,64 @@
<!DOCTYPE html>
<html>
- <head>
- <title>Deploy in progress</title>
- <style>
- body {
- color: #666;
- text-align: center;
- font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
- margin: 0;
- width: 800px;
- margin: auto;
- font-size: 14px;
- }
+<head>
+ <meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
+ <title>Deploy in progress</title>
+ <style>
+ 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;
- }
+ h1 {
+ font-size: 56px;
+ line-height: 100px;
+ font-weight: normal;
+ color: #456;
+ }
- h2 {
- font-size: 24px;
- color: #666;
- line-height: 1.5em;
- }
+ h2 {
+ font-size: 24px;
+ color: #666;
+ line-height: 1.5em;
+ }
- h3 {
- color: #456;
- font-size: 20px;
- font-weight: normal;
- line-height: 28px;
- }
+ h3 {
+ color: #456;
+ font-size: 20px;
+ font-weight: normal;
+ line-height: 28px;
+ }
- hr {
- margin: 18px 0;
- border: 0;
- border-top: 1px solid #EEE;
- border-bottom: 1px solid white;
- }
- </style>
- </head>
+ hr {
+ max-width: 800px;
+ margin: 18px auto;
+ border: 0;
+ border-top: 1px solid #EEE;
+ border-bottom: 1px solid white;
+ }
- <body>
- <h1>
- <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" /><br />
- Deploy in progress
- </h1>
+ img {
+ max-width: 40vw;
+ }
+
+ .container {
+ margin: auto 20px;
+ }
+ </style>
+</head>
+
+<body>
+ <h1>
+ <img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjEwIiBoZWlnaHQ9IjIxMCIgdmlld0JveD0iMCAwIDIxMCAyMTAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTVsMzguNjQtMTE4LjkyMWgtNzcuMjhsMzguNjQgMTE4LjkyMXoiIGZpbGw9IiNlMjQzMjkiLz4KICA8cGF0aCBkPSJNMTA1LjA2MTQgMjAzLjY1NDhsLTM4LjY0LTExOC45MjFoLTU0LjE1M2w5Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTIuMjY4NSA4NC43MzQxbC0xMS43NDIgMzYuMTM5Yy0xLjA3MSAzLjI5Ni4xMDIgNi45MDcgMi45MDYgOC45NDRsMTAxLjYyOSA3My44MzgtOTIuNzkzLTExOC45MjF6IiBmaWxsPSIjZmNhMzI2Ii8+CiAgPHBhdGggZD0iTTEyLjI2ODUgODQuNzM0Mmg1NC4xNTNsLTIzLjI3My03MS42MjVjLTEuMTk3LTMuNjg2LTYuNDExLTMuNjg1LTcuNjA4IDBsLTIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+CiAgPHBhdGggZD0iTTEwNS4wNjE0IDIwMy42NTQ4bDM4LjY0LTExOC45MjFoNTQuMTUzbC05Mi43OTMgMTE4LjkyMXoiIGZpbGw9IiNmYzZkMjYiLz4KICA8cGF0aCBkPSJNMTk3Ljg1NDQgODQuNzM0MWwxMS43NDIgMzYuMTM5YzEuMDcxIDMuMjk2LS4xMDIgNi45MDctMi45MDYgOC45NDRsLTEwMS42MjkgNzMuODM4IDkyLjc5My0xMTguOTIxeiIgZmlsbD0iI2ZjYTMyNiIvPgogIDxwYXRoIGQ9Ik0xOTcuODU0NCA4NC43MzQyaC01NC4xNTNsMjMuMjczLTcxLjYyNWMxLjE5Ny0zLjY4NiA2LjQxMS0zLjY4NSA3LjYwOCAwbDIzLjI3MiA3MS42MjV6IiBmaWxsPSIjZTI0MzI5Ii8+Cjwvc3ZnPgo=" alt="GitLab Logo" /><br />
+ Deploy in progress
+ </h1>
+ <div class="container">
<h3>Please try again in a few minutes.</h3>
- <hr/>
+ <hr />
<p>Please contact your GitLab administrator if this problem persists.</p>
- </body>
+ </div>
+</body>
</html>
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
new file mode 100755
index 00000000000..bc6e4d94061
--- /dev/null
+++ b/scripts/lint-doc.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+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/
+if [ $? == 0 ]
+then
+ echo '✖ ERROR: Short options should not be used in documentation!' >&2
+ exit 1
+fi
+
+echo "✔ Linting passed"
+exit 0
+
diff --git a/scripts/merge-simplecov b/scripts/merge-simplecov
new file mode 100755
index 00000000000..65f93f8830b
--- /dev/null
+++ b/scripts/merge-simplecov
@@ -0,0 +1,30 @@
+#!/usr/bin/env ruby
+
+require_relative '../spec/simplecov_env'
+SimpleCovEnv.configure_profile
+
+module SimpleCov
+ module ResultMerger
+ class << self
+ def resultset_files
+ Dir.glob(File.join(SimpleCov.coverage_path, '*', '.resultset.json'))
+ end
+
+ def resultset_hashes
+ resultset_files.map do |path|
+ begin
+ JSON.parse(File.read(path))
+ rescue
+ {}
+ end
+ end
+ end
+
+ def resultset
+ resultset_hashes.reduce({}, :merge)
+ end
+ end
+ end
+end
+
+SimpleCov::ResultMerger.merged_result.format!
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 7e71a030901..76b2178c79c 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -20,10 +20,11 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
# Install phantomjs package
pushd vendor/apt
- if [ ! -e phantomjs_1.9.8-0jessie_amd64.deb ]; then
- wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+ PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64"
+ if [ ! -d "$PHANTOMJS_FILE" ]; then
+ curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx
fi
- dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
+ cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
popd
# Try to install packages
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 6fad7e2b9e7..c5d3cd70acc 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,53 +1,48 @@
-require "spec_helper"
+require 'spec_helper'
-describe "mail_room.yml" do
- let(:config_path) { "config/mail_room.yml" }
+describe 'mail_room.yml' do
+ let(:config_path) { 'config/mail_room.yml' }
let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
- context "when incoming email is disabled" do
+ context 'when incoming email is disabled' do
before do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_disabled.yml').to_s
+ Gitlab::MailRoom.reset_config!
end
after do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
end
- it "contains no configuration" do
+ it 'contains no configuration' do
expect(configuration[:mailboxes]).to be_nil
end
end
- context "when incoming email is enabled" do
+ context 'when incoming email is enabled' do
before do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
+ Gitlab::MailRoom.reset_config!
end
after do
- ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
end
- it "contains the intended configuration" do
+ it 'contains the intended configuration' do
expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
- expect(mailbox[:host]).to eq("imap.gmail.com")
+ expect(mailbox[:host]).to eq('imap.gmail.com')
expect(mailbox[:port]).to eq(993)
expect(mailbox[:ssl]).to eq(true)
expect(mailbox[:start_tls]).to eq(false)
- expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com")
- expect(mailbox[:password]).to eq("[REDACTED]")
- expect(mailbox[:name]).to eq("inbox")
-
- redis_config_file = Rails.root.join('config', 'resque.yml')
-
- redis_url =
- if File.exist?(redis_config_file)
- YAML.load_file(redis_config_file)[Rails.env]
- else
- "redis://localhost:6379"
- end
+ expect(mailbox[:email]).to eq('gitlab-incoming@gmail.com')
+ expect(mailbox[:password]).to eq('[REDACTED]')
+ expect(mailbox[:name]).to eq('inbox')
+
+ redis_url = Gitlab::Redis.url
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
new file mode 100644
index 00000000000..602de72d23f
--- /dev/null
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Admin::GroupsController do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'DELETE #destroy' do
+ it 'schedules a group destroy' do
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: project.group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'redirects to the admin group path' do
+ delete :destroy, id: project.group.path
+
+ expect(response).to redirect_to(admin_groups_path)
+ end
+ end
+end
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index d5f0b289b5b..8be662974a0 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -77,6 +77,8 @@ describe Admin::ImpersonationsController do
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
+
delete :destroy
expect(response).to redirect_to(admin_user_path(user))
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 520a4f6f9c5..585ca31389d 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -34,4 +34,16 @@ describe Admin::SpamLogsController do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#mark_as_ham' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+ end
+ it 'submits the log as ham' do
+ post :mark_as_ham, id: first_spam.id
+
+ expect(response).to have_http_status(302)
+ expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
+ end
+ end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index ab9aa65f7b9..33fe3c73822 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -39,7 +39,7 @@ describe Admin::UsersController do
user.ldap_block
end
- it 'will not unblock user' do
+ it 'does not unblock user' do
put :unblock, id: user.username
user.reload
expect(user.blocked?).to be_truthy
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 8bd210cbc3d..98e912f000c 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -5,7 +5,7 @@ describe ApplicationController do
let(:user) { create(:user) }
let(:controller) { ApplicationController.new }
- it 'should redirect if the user is over their password expiry' do
+ it 'redirects if the user is over their password expiry' do
user.password_expires_at = Time.new(2002)
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -14,7 +14,7 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
- it 'should not redirect if the user is under their password expiry' do
+ it 'does not redirect if the user is under their password expiry' do
user.password_expires_at = Time.now + 20010101
expect(user.ldap_user?).to be_falsey
allow(controller).to receive(:current_user).and_return(user)
@@ -22,7 +22,7 @@ describe ApplicationController do
controller.send(:check_password_expiration)
end
- it 'should not redirect if the user is over their password expiry but they are an ldap user' do
+ it 'does not redirect if the user is over their password expiry but they are an ldap user' do
user.password_expires_at = Time.new(2002)
allow(user).to receive(:ldap_user?).and_return(true)
allow(controller).to receive(:current_user).and_return(user)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 60c654f622d..a121cb2fc97 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -2,165 +2,312 @@ require 'spec_helper'
describe AutocompleteController do
let!(:project) { create(:project) }
- let!(:user) { create(:user) }
- let!(:user2) { create(:user) }
- let!(:non_member) { create(:user) }
+ let!(:user) { create(:user) }
- context 'project members' do
- before do
- sign_in(user)
- project.team << [user, :master]
- end
+ context 'users and members' do
+ let!(:user2) { create(:user) }
+ let!(:non_member) { create(:user) }
- describe 'GET #users with project ID' do
+ context 'project members' do
before do
- get(:users, project_id: project.id)
+ sign_in(user)
+ project.team << [user, :master]
end
- let(:body) { JSON.parse(response.body) }
+ describe 'GET #users with project ID' do
+ before do
+ get(:users, project_id: project.id)
+ end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
- it { expect(body.map { |u| u["username"] }).to include(user.username) }
+ let(:body) { JSON.parse(response.body) }
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ it { expect(body.map { |u| u["username"] }).to include(user.username) }
+ end
+
+ describe 'GET #users with unknown project' do
+ before do
+ get(:users, project_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- describe 'GET #users with unknown project' do
+ context 'group members' do
+ let(:group) { create(:group) }
+
before do
- get(:users, project_id: 'unknown')
+ sign_in(user)
+ group.add_owner(user)
end
- it { expect(response).to have_http_status(404) }
- end
- end
+ let(:body) { JSON.parse(response.body) }
- context 'group members' do
- let(:group) { create(:group) }
+ describe 'GET #users with group ID' do
+ before do
+ get(:users, group_id: group.id)
+ end
- before do
- sign_in(user)
- group.add_owner(user)
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ it { expect(body.first["username"]).to eq user.username }
+ end
+
+ describe 'GET #users with unknown group ID' do
+ before do
+ get(:users, group_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'non-member login for public project' do
+ let!(:project) { create(:project, :public) }
- describe 'GET #users with group ID' do
before do
- get(:users, group_id: group.id)
+ sign_in(non_member)
+ project.team << [user, :master]
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
- it { expect(body.first["username"]).to eq user.username }
+ let(:body) { JSON.parse(response.body) }
+
+ describe 'GET #users with project ID' do
+ before do
+ get(:users, project_id: project.id, current_user: true)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 2 }
+ it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+ end
end
- describe 'GET #users with unknown group ID' do
+ context 'all users' do
before do
- get(:users, group_id: 'unknown')
+ sign_in(user)
+ get(:users)
end
- it { expect(response).to have_http_status(404) }
+ let(:body) { JSON.parse(response.body) }
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq User.count }
end
- end
- context 'non-member login for public project' do
- let!(:project) { create(:project, :public) }
+ context 'unauthenticated user' do
+ let(:public_project) { create(:project, :public) }
+ let(:body) { JSON.parse(response.body) }
- before do
- sign_in(non_member)
- project.team << [user, :master]
- end
+ describe 'GET #users with public project' do
+ before do
+ public_project.team << [user, :guest]
+ get(:users, project_id: public_project.id)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 1 }
+ end
- let(:body) { JSON.parse(response.body) }
+ describe 'GET #users with project' do
+ before do
+ get(:users, project_id: project.id)
+ end
- describe 'GET #users with project ID' do
- before do
- get(:users, project_id: project.id, current_user: true)
+ it { expect(response).to have_http_status(404) }
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 2 }
- it { expect(body.map { |u| u['username'] }).to match_array([user.username, non_member.username]) }
+ describe 'GET #users with unknown project' do
+ before do
+ get(:users, project_id: 'unknown')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ describe 'GET #users with inaccessible group' do
+ before do
+ project.team << [user, :guest]
+ get(:users, group_id: user.namespace.id)
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
+
+ describe 'GET #users with no project' do
+ before do
+ get(:users)
+ end
+
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 0 }
+ end
end
- end
- context 'all users' do
- before do
- sign_in(user)
- get(:users)
+ context 'author of issuable included' do
+ before do
+ sign_in(user)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it 'includes the author' do
+ get(:users, author_id: non_member.id)
+
+ expect(body.first["username"]).to eq non_member.username
+ end
+
+ it 'rejects non existent user ids' do
+ get(:users, author_id: 99999)
+
+ expect(body.collect { |u| u['id'] }).not_to include(99999)
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'skip_users parameter included' do
+ before { sign_in(user) }
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq User.count }
- end
+ it 'skips the user IDs passed' do
+ get(:users, skip_users: [user, user2].map(&:id))
- context 'unauthenticated user' do
- let(:public_project) { create(:project, :public) }
- let(:body) { JSON.parse(response.body) }
+ other_user_ids = [non_member, project.owner, project.creator].map(&:id)
+ response_user_ids = JSON.parse(response.body).map { |user| user['id'] }
- describe 'GET #users with public project' do
- before do
- public_project.team << [user, :guest]
- get(:users, project_id: public_project.id)
+ expect(response_user_ids).to contain_exactly(*other_user_ids)
end
+ end
+ end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 1 }
+ context 'projects' do
+ let(:authorized_project) { create(:project) }
+ let(:authorized_search_project) { create(:project, name: 'rugged') }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
end
- describe 'GET #users with project' do
+ context 'authorized projects' do
before do
- get(:users, project_id: project.id)
+ authorized_project.team << [user, :master]
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 2
+
+ expect(body.first['id']).to eq 0
+ expect(body.first['name_with_namespace']).to eq 'No project'
+
+ expect(body.last['id']).to eq authorized_project.id
+ expect(body.last['name_with_namespace']).to eq authorized_project.name_with_namespace
+ end
+ end
end
- describe 'GET #users with unknown project' do
+ context 'authorized projects and search' do
before do
- get(:users, project_id: 'unknown')
+ authorized_project.team << [user, :master]
+ authorized_search_project.team << [user, :master]
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID and search' do
+ before do
+ get(:projects, project_id: project.id, search: 'rugged')
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 2
+
+ expect(body.last['id']).to eq authorized_search_project.id
+ expect(body.last['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+ end
+ end
end
- describe 'GET #users with inaccessible group' do
+ context 'authorized projects apply limit' do
before do
- project.team << [user, :guest]
- get(:users, group_id: user.namespace.id)
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
end
- it { expect(response).to have_http_status(404) }
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 3 # Of a total of 4
+ end
+ end
end
- describe 'GET #users with no project' do
+ context 'authorized projects with offset' do
before do
- get(:users)
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
end
- it { expect(body).to be_kind_of(Array) }
- it { expect(body.size).to eq 0 }
- end
- end
+ describe 'GET #projects with project ID and offset_id' do
+ before do
+ get(:projects, project_id: project.id, offset_id: authorized_project.id)
+ end
- context 'author of issuable included' do
- before do
- sign_in(user)
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
+ expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+ end
+ end
end
- let(:body) { JSON.parse(response.body) }
+ context 'authorized projects without admin_issue ability' do
+ before(:each) do
+ authorized_project.team << [user, :guest]
+
+ expect(user.can?(:admin_issue, authorized_project)).to eq(false)
+ end
- it 'includes the author' do
- get(:users, author_id: non_member.id)
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
- expect(body.first["username"]).to eq non_member.username
- end
+ let(:body) { JSON.parse(response.body) }
- it 'rejects non existent user ids' do
- get(:users, author_id: 99999)
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 1 # 'No project'
- expect(body.collect { |u| u['id'] }).not_to include(99999)
+ expect(body.first['id']).to eq 0
+ end
+ end
end
end
end
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
deleted file mode 100644
index a3a3309e15e..00000000000
--- a/spec/controllers/commit_controller_spec.rb
+++ /dev/null
@@ -1,246 +0,0 @@
-require 'spec_helper'
-
-describe Projects::CommitController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:commit) { project.commit("master") }
- let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
- let(:master_pickable_commit) { project.commit(master_pickable_sha) }
-
- before do
- sign_in(user)
- project.team << [user, :master]
- end
-
- describe "#show" do
- shared_examples "export as" do |format|
- it "should generally work" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response).to be_success
- end
-
- it "should generate it" do
- expect_any_instance_of(Commit).to receive(:"to_#{format}")
-
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
- end
-
- it "should render it" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
-
- expect(response.body).to eq(commit.send(:"to_#{format}"))
- end
-
- it "should not escape Html" do
- allow_any_instance_of(Commit).to receive(:"to_#{format}").
- and_return('HTML entities &<>" ')
-
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id, format: format)
-
- expect(response.body).not_to include('&amp;')
- expect(response.body).not_to include('&gt;')
- expect(response.body).not_to include('&lt;')
- expect(response.body).not_to include('&quot;')
- end
- end
-
- describe "as diff" do
- include_examples "export as", :diff
- let(:format) { :diff }
-
- it "should really only be a git diff" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to start_with("diff --git")
- end
-
- it "should really only be a git diff without whitespace changes" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: '66eceea0db202bb39c4e445e8ca28689645366c5',
- # id: commit.id,
- format: format,
- w: 1)
-
- expect(response.body).to start_with("diff --git")
- # without whitespace option, there are more than 2 diff_splits
- diff_splits = assigns(:diffs).first.diff.split("\n")
- expect(diff_splits.length).to be <= 2
- end
- end
-
- describe "as patch" do
- include_examples "export as", :patch
- let(:format) { :patch }
-
- it "should really be a git email patch" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to start_with("From #{commit.id}")
- end
-
- it "should contain a git diff" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id,
- format: format)
-
- expect(response.body).to match(/^diff --git/)
- end
- end
-
- context 'commit that removes a submodule' do
- render_views
-
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:commit) { fork_project.commit('remove-submodule') }
-
- before do
- fork_project.team << [user, :master]
- end
-
- it 'renders it' do
- get(:show,
- namespace_id: fork_project.namespace.to_param,
- project_id: fork_project.to_param,
- id: commit.id)
-
- expect(response).to be_success
- end
- end
- end
-
- describe "#branches" do
- it "contains branch and tags information" do
- get(:branches,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id)
-
- expect(assigns(:branches)).to include("master", "feature_conflict")
- expect(assigns(:tags)).to include("v1.1.0")
- end
- end
-
- describe '#revert' do
- context 'when target branch is not provided' do
- it 'should render the 404 page' do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: commit.id)
-
- expect(response).not_to be_success
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when the revert was successful' do
- it 'should redirect to the commits page' do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
-
- expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
- expect(flash[:notice]).to eq('The commit has been successfully reverted.')
- end
- end
-
- context 'when the revert failed' do
- before do
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
- end
-
- it 'should redirect to the commit page' do
- # Reverting a commit that has been already reverted.
- post(:revert,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: commit.id)
-
- expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
- end
- end
- end
-
- describe '#cherry_pick' do
- context 'when target branch is not provided' do
- it 'should render the 404 page' do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: master_pickable_commit.id)
-
- expect(response).not_to be_success
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when the cherry-pick was successful' do
- it 'should redirect to the commits page' do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
-
- expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
- expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
- end
- end
-
- context 'when the cherry_pick failed' do
- before do
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
- end
-
- it 'should redirect to the commit page' do
- # Cherry-picking a commit that has been already cherry-picked.
- post(:cherry_pick,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- target_branch: 'master',
- id: master_pickable_commit.id)
-
- expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
- expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
- end
- end
- end
-end
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 91d639218e5..506aeee7d2a 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -9,7 +9,7 @@ describe Groups::AvatarsController do
sign_in(user)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB calling destroy' do
delete :destroy, group_id: group.path
@group = assigns(:group)
expect(@group.avatar.present?).to be_falsey
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index b0793cb1655..8c52f615b8b 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -15,7 +15,7 @@ describe Groups::MilestonesController do
end
describe "#create" do
- it "should create group milestone with Chinese title" do
+ it "creates group milestone with Chinese title" do
post :create,
group_id: group.id,
milestone: { project_ids: [project.id, project2.id], title: title }
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index cd98fecd0c7..a763e2c5ba8 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -75,4 +75,34 @@ describe GroupsController do
end
end
end
+
+ describe 'DELETE #destroy' do
+ context 'as another user' do
+ it 'returns 404' do
+ sign_in(create(:user))
+
+ delete :destroy, id: group.path
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'as the group owner' do
+ before do
+ sign_in(user)
+ end
+
+ it 'schedules a group destroy' do
+ Sidekiq::Testing.fake! do
+ expect { delete :destroy, id: group.path }.to change(GroupDestroyWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'redirects to the root path' do
+ delete :destroy, id: group.path
+
+ expect(response).to redirect_to(root_path)
+ end
+ end
+ end
end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index 1f7fd517342..33c75e7584f 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -11,7 +11,7 @@ describe HelpController do
context 'for Markdown formats' do
context 'when requested file exists' do
before do
- get :show, category: 'ssh', file: 'README', format: :md
+ get :show, path: 'ssh/README', format: :md
end
it 'assigns to @markdown' do
@@ -26,7 +26,7 @@ describe HelpController do
context 'when requested file is missing' do
it 'renders not found' do
- get :show, category: 'foo', file: 'bar', format: :md
+ get :show, path: 'foo/bar', format: :md
expect(response).to be_not_found
end
end
@@ -36,8 +36,7 @@ describe HelpController do
context 'when requested file exists' do
it 'renders the raw file' do
get :show,
- category: 'workflow/protected_branches',
- file: 'protected_branches1',
+ path: 'user/project/img/labels_filter',
format: :png
expect(response).to be_success
expect(response.content_type).to eq 'image/png'
@@ -48,8 +47,7 @@ describe HelpController do
context 'when requested file is missing' do
it 'renders not found' do
get :show,
- category: 'foo',
- file: 'bar',
+ path: 'foo/bar',
format: :png
expect(response).to be_not_found
end
@@ -59,11 +57,19 @@ describe HelpController do
context 'for other formats' do
it 'always renders not found' do
get :show,
- category: 'ssh',
- file: 'README',
+ path: 'ssh/README',
format: :foo
expect(response).to be_not_found
end
end
end
+
+ describe 'GET #ui' do
+ context 'for UI Development Kit' do
+ it 'renders found' do
+ get :ui
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
deleted file mode 100644
index 4ae2b78e11c..00000000000
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Import::GitoriousController do
- include ImportSpecHelper
-
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- describe "GET new" do
- it "redirects to import endpoint on gitorious.org" do
- get :new
-
- expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback")
- end
- end
-
- describe "GET callback" do
- it "stores repo list in session" do
- get :callback, repos: 'foo/bar,baz/qux'
-
- expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux')
- end
- end
-
- describe "GET status" do
- before do
- @repo = OpenStruct.new(full_name: 'asd/vim')
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id)
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'gitorious', creator_id: user.id, import_source: 'asd/vim')
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
- end
-
- describe "POST create" do
- before do
- @repo = Gitlab::GitoriousImport::Repository.new('asd/vim')
- end
-
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "asd", owner: user)
- expect(Gitlab::GitoriousImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- stub_client(repo: @repo)
-
- post :create, format: :js
- end
- end
-end
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index ad5855df0a4..4fa0462ccdf 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -8,7 +8,7 @@ describe Profiles::AvatarsController do
controller.instance_variable_set(:@user, user)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB by calling destroy' do
delete :destroy
@user = assigns(:user)
expect(@user.avatar.present?).to be_falsey
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 3a82083717f..6bcfae0fc13 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -6,7 +6,7 @@ describe Profiles::KeysController do
describe '#new' do
before { sign_in(user) }
- it 'redirect to #index' do
+ it 'redirects to #index' do
get :new
expect(response).to redirect_to(profile_keys_path)
@@ -15,7 +15,7 @@ describe Profiles::KeysController do
describe "#get_keys" do
describe "non existant user" do
- it "should generally not work" do
+ it "does not generally work" do
get :get_keys, username: 'not-existent'
expect(response).not_to be_success
@@ -23,19 +23,19 @@ describe Profiles::KeysController do
end
describe "user with no keys" do
- it "should generally work" do
+ it "does generally work" do
get :get_keys, username: user.username
expect(response).to be_success
end
- it "should render all keys separated with a new line" do
+ it "renders all keys separated with a new line" do
get :get_keys, username: user.username
expect(response.body).to eq("")
end
- it "should respond with text/plain content type" do
+ it "responds with text/plain content type" do
get :get_keys, username: user.username
expect(response.content_type).to eq("text/plain")
end
@@ -47,13 +47,13 @@ describe Profiles::KeysController do
user.keys << create(:another_key)
end
- it "should generally work" do
+ it "does generally work" do
get :get_keys, username: user.username
expect(response).to be_success
end
- it "should render all keys separated with a new line" do
+ it "renders all keys separated with a new line" do
get :get_keys, username: user.username
expect(response.body).not_to eq("")
@@ -65,13 +65,13 @@ describe Profiles::KeysController do
expect(response.body).to match(/AQDmTillFzNTrrGgwaCKaSj/)
end
- it "should not render the comment of the key" do
+ it "does not render the comment of the key" do
get :get_keys, username: user.username
expect(response.body).not_to match(/dummy@gitlab.com/)
end
- it "should respond with text/plain content type" do
+ it "responds with text/plain content type" do
get :get_keys, username: user.username
expect(response.content_type).to eq("text/plain")
end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 4d724ca9ed0..f5ea097af8b 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -10,7 +10,7 @@ describe Projects::AvatarsController do
controller.instance_variable_set(:@project, project)
end
- it 'destroy should remove avatar from DB' do
+ it 'removes avatar from DB by calling destroy' do
delete :destroy, namespace_id: project.namespace.id, project_id: project.id
expect(project.avatar.present?).to be_falsey
expect(project).to be_valid
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
new file mode 100644
index 00000000000..d0ad5e26dbd
--- /dev/null
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Projects::Boards::IssuesController do
+ let(:project) { create(:project_with_board) }
+ let(:user) { create(:user) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET index' do
+ context 'with valid list id' do
+ it 'returns issues that have the list label applied' do
+ johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ create(:labeled_issue, project: project, labels: [planning])
+ create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+
+ list_issues user: user, list_id: list2
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('issues')
+ expect(parsed_response.length).to eq 2
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ list_issues user: user, list_id: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_issue, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ list_issues user: user, list_id: list2
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def list_issues(user:, list_id:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list_id: list_id.to_param
+ end
+ end
+
+ describe 'PATCH update' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves issue to the desired list' do
+ move user: user, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(issue.reload.labels).to contain_exactly(development)
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns a unprocessable entity 422 response for invalid lists' do
+ move user: user, issue: issue, from_list_id: nil, to_list_id: nil
+
+ expect(response).to have_http_status(422)
+ end
+
+ it 'returns a not found 404 response for invalid issue id' do
+ move user: user, issue: 999, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'returns a successful 403 response' do
+ move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, issue:, from_list_id:, to_list_id:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param,
+ from_list_id: from_list_id,
+ to_list_id: to_list_id,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
new file mode 100644
index 00000000000..9496636e3cc
--- /dev/null
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -0,0 +1,241 @@
+require 'spec_helper'
+
+describe Projects::Boards::ListsController do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ project.team << [guest, :guest]
+ end
+
+ describe 'GET index' do
+ it 'returns a successful 200 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+ end
+
+ it 'returns a list of board lists' do
+ board = project.create_board
+ create(:backlog_list, board: board)
+ create(:list, board: board)
+ create(:done_list, board: board)
+
+ read_board_list user: user
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(response).to match_response_schema('lists')
+ expect(parsed_response.length).to eq 3
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
+ end
+
+ it 'returns a successful 403 response' do
+ read_board_list user: user
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def read_board_list(user:)
+ sign_in(user)
+
+ get :index, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST create' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created list' do
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to match_response_schema('list')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'returns an error' do
+ create_board_list user: user, label_id: nil
+
+ parsed_response = JSON.parse(response.body)
+
+ expect(parsed_response['label']).to contain_exactly "can't be blank"
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
+ it 'returns a successful 403 response' do
+ create_board_list user: guest, label_id: label.id
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_board_list(user:, label_id:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list: { label_id: label_id },
+ format: :json
+ end
+ end
+
+ describe 'PATCH update' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+
+ context 'with valid position' do
+ it 'returns a successful 200 response' do
+ move user: user, list: planning, position: 1
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'moves the list to the desired position' do
+ move user: user, list: planning, position: 1
+
+ expect(planning.reload.position).to eq 1
+ end
+ end
+
+ context 'with invalid position' do
+ it 'returns a unprocessable entity 422 response' do
+ move user: user, list: planning, position: 6
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ move user: user, list: 999, position: 1
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ move user: guest, list: planning, position: 6
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def move(user:, list:, position:)
+ sign_in(user)
+
+ patch :update, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ list: { position: position },
+ format: :json
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let!(:planning) { create(:list, board: board, position: 0) }
+
+ context 'with valid list id' do
+ it 'returns a successful 200 response' do
+ remove_board_list user: user, list: planning
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'removes list from board' do
+ expect { remove_board_list user: user, list: planning }.to change(board.lists, :size).by(-1)
+ end
+ end
+
+ context 'with invalid list id' do
+ it 'returns a not found 404 response' do
+ remove_board_list user: user, list: 999
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ remove_board_list user: guest, list: planning
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def remove_board_list(user:, list:)
+ sign_in(user)
+
+ delete :destroy, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: list.to_param,
+ format: :json
+ end
+ end
+
+ describe 'POST generate' do
+ context 'when board lists is empty' do
+ it 'returns a successful 200 response' do
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the defaults lists' do
+ generate_default_board_lists user: user
+
+ expect(response).to match_response_schema('lists')
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'returns a unprocessable entity 422 response' do
+ create(:list, board: board)
+
+ generate_default_board_lists user: user
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a successful 403 response' do
+ generate_default_board_lists user: guest
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def generate_default_board_lists(user:)
+ sign_in(user)
+
+ post :generate, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: :json
+ end
+ end
+end
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
new file mode 100644
index 00000000000..75a6d39e82c
--- /dev/null
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::BoardsController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'creates a new board when project does not have one' do
+ expect { read_board }.to change(Board, :count).by(1)
+ end
+
+ it 'renders HTML template' do
+ read_board
+
+ expect(response).to render_template :show
+ expect(response.content_type).to eq 'text/html'
+ end
+
+ context 'with unauthorized user' do
+ before do
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_project, project).and_return(true)
+ allow(Ability.abilities).to receive(:allowed?).with(user, :read_board, project).and_return(false)
+ end
+
+ it 'returns a successful 404 response' do
+ read_board
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ def read_board(format: :html)
+ get :show, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ format: format
+ end
+ end
+end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 6e3db10e451..7e440193d7b 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,21 +1,32 @@
-require 'rails_helper'
+require 'spec_helper'
describe Projects::CommitController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit) { project.commit("master") }
+ let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
+ let(:master_pickable_commit) { project.commit(master_pickable_sha) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
describe 'GET show' do
render_views
- let(:project) { create(:project) }
-
- before do
- user = create(:user)
- project.team << [user, :master]
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
- sign_in(user)
+ get :show, params.merge(extra_params)
end
context 'with valid id' do
it 'responds with 200' do
- go id: project.commit.id
+ go(id: commit.id)
expect(response).to be_ok
end
@@ -23,27 +34,275 @@ describe Projects::CommitController do
context 'with invalid id' do
it 'responds with 404' do
- go id: project.commit.id.reverse
+ go(id: commit.id.reverse)
expect(response).to be_not_found
end
end
it 'handles binary files' do
- get(:show,
+ go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html')
+
+ expect(response).to be_success
+ end
+
+ shared_examples "export as" do |format|
+ it "does generally work" do
+ go(id: commit.id, format: format)
+
+ expect(response).to be_success
+ end
+
+ it "generates it" do
+ expect_any_instance_of(Commit).to receive(:"to_#{format}")
+
+ go(id: commit.id, format: format)
+ end
+
+ it "renders it" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to eq(commit.send(:"to_#{format}"))
+ end
+
+ it "does not escape Html" do
+ allow_any_instance_of(Commit).to receive(:"to_#{format}").
+ and_return('HTML entities &<>" ')
+
+ go(id: commit.id, format: format)
+
+ expect(response.body).not_to include('&amp;')
+ expect(response.body).not_to include('&gt;')
+ expect(response.body).not_to include('&lt;')
+ expect(response.body).not_to include('&quot;')
+ end
+ end
+
+ describe "as diff" do
+ include_examples "export as", :diff
+ let(:format) { :diff }
+
+ it "should really only be a git diff" do
+ go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
+
+ expect(response.body).to start_with("diff --git")
+ end
+
+ it "is only be a git diff without whitespace changes" do
+ go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1)
+
+ expect(response.body).to start_with("diff --git")
+
+ # without whitespace option, there are more than 2 diff_splits for other formats
+ diff_splits = assigns(:diffs).diff_files.first.diff.diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+ end
+
+ describe "as patch" do
+ include_examples "export as", :patch
+ let(:format) { :patch }
+
+ it "is a git email patch" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to start_with("From #{commit.id}")
+ end
+
+ it "contains a git diff" do
+ go(id: commit.id, format: format)
+
+ expect(response.body).to match(/^diff --git/)
+ end
+ end
+
+ context 'commit that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules, visibility_level: 20) }
+ let(:commit) { fork_project.commit('remove-submodule') }
+
+ it 'renders it' do
+ get(:show,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ id: commit.id)
+
+ expect(response).to be_success
+ end
+ end
+ end
+
+ describe "GET branches" do
+ it "contains branch and tags information" do
+ get(:branches,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
- id: TestEnv::BRANCH_SHA['binary-encoding'],
- format: "html")
+ id: commit.id)
- expect(response).to be_success
+ expect(assigns(:branches)).to include("master", "feature_conflict")
+ expect(assigns(:tags)).to include("v1.1.0")
+ end
+ end
+
+ describe 'POST revert' do
+ context 'when target branch is not provided' do
+ it 'renders the 404 page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: commit.id)
+
+ expect(response).not_to be_success
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the revert was successful' do
+ it 'redirects to the commits page' do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully reverted.')
+ end
+ end
+
+ context 'when the revert failed' do
+ before do
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+ end
+
+ it 'redirects to the commit page' do
+ # Reverting a commit that has been already reverted.
+ post(:revert,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+ end
+
+ describe 'POST cherry_pick' do
+ context 'when target branch is not provided' do
+ it 'renders the 404 page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: master_pickable_commit.id)
+
+ expect(response).not_to be_success
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the cherry-pick was successful' do
+ it 'redirects to the commits page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
+ end
end
- def go(id:)
- get :show,
+ context 'when the cherry_pick failed' do
+ before do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+ end
+
+ it 'redirects to the commit page' do
+ # Cherry-picking a commit that has been already cherry-picked.
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+ end
+
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
+ params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: id
+ project_id: project.to_param
+ }
+
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ let(:existing_path) { '.gitmodules' }
+
+ context 'when the commit exists' do
+ context 'when the user has access to the project' do
+ context 'when the path exists in the diff' do
+ it 'enables diff notes' do
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_falsey
+ expect(assigns(:comments_target)).to eq(noteable_type: 'Commit',
+ commit_id: commit.id)
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(id: commit.id, old_path: existing_path.succ, new_path: existing_path.succ) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ diff_for_path(id: commit.id, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the commit does not exist' do
+ before { diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
end
end
end
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 7d8089c4bc6..2518a48e336 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CommitsController do
describe "GET show" do
context "as atom feed" do
- it "should render as atom" do
+ it "renders as atom" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 4018dac95a2..7a57801c437 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -11,7 +11,7 @@ describe Projects::CompareController do
project.team << [user, :master]
end
- it 'compare should show some diffs' do
+ it 'compare shows some diffs' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -19,11 +19,11 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).first).not_to be_nil
+ expect(assigns(:diffs).diff_files.first).not_to be_nil
expect(assigns(:commits).length).to be >= 1
end
- it 'compare should show some diffs with ignore whitespace change option' do
+ it 'compare shows some diffs with ignore whitespace change option' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -32,15 +32,16 @@ describe Projects::CompareController do
w: 1)
expect(response).to be_success
- expect(assigns(:diffs).first).not_to be_nil
+ diff_file = assigns(:diffs).diff_files.first
+ expect(diff_file).not_to be_nil
expect(assigns(:commits).length).to be >= 1
# without whitespace option, there are more than 2 diff_splits
- diff_splits = assigns(:diffs).first.diff.split("\n")
+ diff_splits = diff_file.diff.diff.split("\n")
expect(diff_splits.length).to be <= 2
end
describe 'non-existent refs' do
- it 'invalid source ref' do
+ it 'uses invalid source ref' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -48,11 +49,11 @@ describe Projects::CompareController do
to: ref_to)
expect(response).to be_success
- expect(assigns(:diffs).to_a).to eq([])
+ expect(assigns(:diffs).diff_files.to_a).to eq([])
expect(assigns(:commits)).to eq([])
end
- it 'invalid target ref' do
+ it 'uses invalid target ref' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -64,4 +65,73 @@ describe Projects::CompareController do
expect(assigns(:commits)).to eq(nil)
end
end
+
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param
+ }
+
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ let(:existing_path) { 'files/ruby/feature.rb' }
+
+ context 'when the from and to refs exist' do
+ context 'when the user has access to the project' do
+ context 'when the path exists in the diff' do
+ it 'disables diff notes' do
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(from: ref_from, to: ref_to, old_path: existing_path.succ, new_path: existing_path.succ) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user does not have access to the project' do
+ before do
+ project.team.truncate
+ diff_for_path(from: ref_from, to: ref_to, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the from ref does not exist' do
+ before { diff_for_path(from: ref_from.succ, to: ref_to, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the to ref does not exist' do
+ before { diff_for_path(from: ref_from, to: ref_to.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
new file mode 100644
index 00000000000..ff617fea847
--- /dev/null
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Projects::DiscussionsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+ let(:discussion) { note.discussion }
+
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ merge_request_id: merge_request,
+ id: note.discussion_id
+ }
+ end
+
+ describe 'POST resolve' do
+ before do
+ sign_in user
+ end
+
+ context "when the user is not authorized to resolve the discussion" do
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the discussion" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the discussion is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the discussion is resolvable" do
+ it "resolves the discussion" do
+ post :resolve, request_params
+
+ expect(note.reload.discussion.resolved?).to be true
+ expect(note.reload.discussion.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ post :resolve, request_params
+ end
+
+ it "returns the name of the resolving user" do
+ post :resolve, request_params
+
+ expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ end
+
+ it "returns status 200" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE unresolve' do
+ before do
+ sign_in user
+
+ note.discussion.resolve!(user)
+ end
+
+ context "when the user is not authorized to resolve the discussion" do
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the discussion" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the discussion is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the discussion is resolvable" do
+ it "unresolves the discussion" do
+ delete :unresolve, request_params
+
+ expect(note.reload.discussion.resolved?).to be false
+ end
+
+ it "returns status 200" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
new file mode 100644
index 00000000000..768105cae95
--- /dev/null
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Projects::EnvironmentsController do
+ let(:environment) { create(:environment) }
+ let(:project) { environment.project }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ context 'with valid id' do
+ it 'responds with a status code 200' do
+ get :show, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ context 'with invalid id' do
+ it 'responds with a status code 404' do
+ params = environment_params
+ params[:id] = 12345
+ get :show, params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET edit' do
+ it 'responds with a status code 200' do
+ get :edit, environment_params
+
+ expect(response).to be_ok
+ end
+ end
+
+ describe 'PATCH #update' do
+ it 'responds with a 302' do
+ patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' })
+ patch :update, patch_params
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ def environment_params
+ {
+ 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 f66bcb8099c..ac3469cb8a9 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -16,7 +16,7 @@ describe Projects::ForksController do
context 'when fork is public' do
before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) }
- it 'should be visible for non logged in users' do
+ it 'is visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_present
@@ -28,7 +28,7 @@ describe Projects::ForksController do
forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
end
- it 'should not be visible for non logged in users' do
+ it 'is not be visible for non logged in users' do
get_forks
expect(assigns[:forks]).to be_blank
@@ -38,7 +38,7 @@ describe Projects::ForksController do
before { sign_in(project.creator) }
context 'when user is not a Project member neither a group member' do
- it 'should not see the Project listed' do
+ it 'does not see the Project listed' do
get_forks
expect(assigns[:forks]).to be_blank
@@ -48,7 +48,7 @@ describe Projects::ForksController do
context 'when user is a member of the Project' do
before { forked_project.team << [project.creator, :developer] }
- it 'should see the project listed' do
+ it 'sees the project listed' do
get_forks
expect(assigns[:forks]).to be_present
@@ -58,7 +58,7 @@ describe Projects::ForksController do
context 'when user is a member of the Group' do
before { forked_project.group.add_developer(project.creator) }
- it 'should see the project listed' do
+ it 'sees the project listed' do
get_forks
expect(assigns[:forks]).to be_present
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 7cf09fa4a4a..16929767ddf 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -6,37 +6,65 @@ describe Projects::IssuesController do
let(:issue) { create(:issue, project: project) }
describe "GET #index" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
+ context 'external issue tracker' do
+ it 'redirects to the external issue tracker' do
+ external = double(project_path: 'https://example.com/project')
+ allow(project).to receive(:external_issue_tracker).and_return(external)
+ controller.instance_variable_set(:@project, project)
- it "returns index" do
- get :index, namespace_id: project.namespace.path, project_id: project.path
+ get :index, namespace_id: project.namespace.path, project_id: project
- expect(response).to have_http_status(200)
+ expect(response).to redirect_to('https://example.com/project')
+ end
end
- it "return 301 if request path doesn't match project path" do
- get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+ context 'internal issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
- expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
- end
+ it "returns index" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path
- it "returns 404 when issues are disabled" do
- project.issues_enabled = false
- project.save
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 301 if request path doesn't match project path" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+
+ expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
+ end
- get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response).to have_http_status(404)
+ it "returns 404 when issues are disabled" do
+ project.issues_enabled = false
+ project.save
+
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 404 when external issue tracker is enabled" do
+ controller.instance_variable_set(:@project, project)
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
+
+ get :index, namespace_id: project.namespace.path, project_id: project.path
+ expect(response).to have_http_status(404)
+ end
end
+ end
+
+ describe 'GET #new' do
+ context 'external issue tracker' do
+ it 'redirects to the external issue tracker' do
+ external = double(new_issue_path: 'https://example.com/issues/new')
+ allow(project).to receive(:external_issue_tracker).and_return(external)
+ controller.instance_variable_set(:@project, project)
- it "returns 404 when external issue tracker is enabled" do
- controller.instance_variable_set(:@project, project)
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ get :new, namespace_id: project.namespace.path, project_id: project
- get :index, namespace_id: project.namespace.path, project_id: project.path
- expect(response).to have_http_status(404)
+ expect(response).to redirect_to('https://example.com/issues/new')
+ end
end
end
@@ -91,21 +119,21 @@ describe Projects::IssuesController do
let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) }
describe 'GET #index' do
- it 'should not list confidential issues for guests' do
+ it 'does not list confidential issues for guests' do
sign_out(:user)
get_issues
expect(assigns(:issues)).to eq [issue]
end
- it 'should not list confidential issues for non project members' do
+ it 'does not list confidential issues for non project members' do
sign_in(non_member)
get_issues
expect(assigns(:issues)).to eq [issue]
end
- it 'should not list confidential issues for project members with guest role' do
+ it 'does not list confidential issues for project members with guest role' do
sign_in(member)
project.team << [member, :guest]
@@ -114,7 +142,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to eq [issue]
end
- it 'should list confidential issues for author' do
+ it 'lists confidential issues for author' do
sign_in(author)
get_issues
@@ -122,7 +150,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).not_to include request_forgery_timing_attack
end
- it 'should list confidential issues for assignee' do
+ it 'lists confidential issues for assignee' do
sign_in(assignee)
get_issues
@@ -130,7 +158,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to include request_forgery_timing_attack
end
- it 'should list confidential issues for project members' do
+ it 'lists confidential issues for project members' do
sign_in(member)
project.team << [member, :developer]
@@ -140,7 +168,7 @@ describe Projects::IssuesController do
expect(assigns(:issues)).to include request_forgery_timing_attack
end
- it 'should list confidential issues for admin' do
+ it 'lists confidential issues for admin' do
sign_in(admin)
get_issues
@@ -243,6 +271,83 @@ describe Projects::IssuesController do
end
end
+ describe 'POST #create' do
+ context 'Akismet is enabled' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
+ end
+
+ def post_spam_issue
+ sign_in(user)
+ spam_project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: spam_project.namespace.to_param,
+ project_id: spam_project.to_param,
+ issue: { title: 'Spam Title', description: 'Spam lives here' }
+ }
+ end
+
+ it 'rejects an issue recognized as spam' do
+ expect{ post_spam_issue }.not_to change(Issue, :count)
+ expect(response).to render_template(:new)
+ end
+
+ it 'creates a spam log' do
+ post_spam_issue
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('Spam Title')
+ end
+ end
+
+ context 'user agent details are saved' do
+ before do
+ request.env['action_dispatch.remote_ip'] = '127.0.0.1'
+ end
+
+ def post_new_issue
+ sign_in(user)
+ project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+
+ it 'creates a user agent detail' do
+ expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ context 'properly submits to Akismet' do
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+ end
+
+ def post_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: issue)
+ project.team << [admin, :master]
+ sign_in(admin)
+ post :mark_as_spam, {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issue.iid
+ }
+ end
+
+ it 'updates issue' do
+ post_spam
+ expect(issue.submittable_as_spam?).to be_falsey
+ end
+ end
+ end
+
describe "DELETE #destroy" do
context "when the user is a developer" do
before { sign_in(user) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c4b57e77804..c64c2b075c5 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -4,13 +4,18 @@ describe Projects::MergeRequestsController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
+ let(:merge_request_with_conflicts) do
+ create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
before do
sign_in(user)
project.team << [user, :master]
end
- describe '#new' do
+ describe 'GET new' do
context 'merge request that removes a submodule' do
render_views
@@ -34,9 +39,9 @@ describe Projects::MergeRequestsController do
end
end
- describe "#show" do
+ describe "GET show" do
shared_examples "export merge as" do |format|
- it "should generally work" do
+ it "does generally work" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -46,7 +51,7 @@ describe Projects::MergeRequestsController do
expect(response).to be_success
end
- it "should generate it" do
+ it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
get(:show,
@@ -56,7 +61,7 @@ describe Projects::MergeRequestsController do
format: format)
end
- it "should render it" do
+ it "renders it" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
@@ -66,7 +71,7 @@ describe Projects::MergeRequestsController do
expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
end
- it "should not escape Html" do
+ it "does not escape Html" do
allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
and_return('HTML entities &<>" ')
@@ -108,7 +113,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'GET #index' do
+ describe 'GET index' do
def get_merge_requests
get :index,
namespace_id: project.namespace.to_param,
@@ -118,7 +123,7 @@ describe Projects::MergeRequestsController do
context 'when filtering by opened state' do
context 'with opened merge requests' do
- it 'should list those merge requests' do
+ it 'lists those merge requests' do
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
@@ -131,7 +136,7 @@ describe Projects::MergeRequestsController do
merge_request.reopen!
end
- it 'should list those merge requests' do
+ it 'lists those merge requests' do
get_merge_requests
expect(assigns(:merge_requests)).to include(merge_request)
@@ -140,7 +145,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'PUT #update' do
+ describe 'PUT update' do
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
@@ -168,7 +173,7 @@ describe Projects::MergeRequestsController do
end
end
- describe 'POST #merge' do
+ describe 'POST merge' do
let(:base_params) do
{
namespace_id: project.namespace.path,
@@ -266,7 +271,7 @@ describe Projects::MergeRequestsController do
end
end
- describe "DELETE #destroy" do
+ describe "DELETE destroy" do
it "denies access to users unless they're admin or project owner" do
delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
@@ -290,96 +295,210 @@ describe Projects::MergeRequestsController do
end
describe 'GET diffs' do
- def go(format: 'html')
- get :diffs,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid,
- format: format
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid
+ }
+
+ get :diffs, params.merge(extra_params)
end
- context 'as html' do
- it 'renders the diff template' do
- go
+ context 'with default params' do
+ context 'as html' do
+ before { go(format: 'html') }
- expect(response).to render_template('diffs')
+ it 'renders the diff template' do
+ expect(response).to render_template('diffs')
+ end
end
- end
- context 'as json' do
- it 'renders the diffs template to a string' do
- go format: 'json'
+ context 'as json' do
+ before { go(format: 'json') }
- expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ it 'renders the diffs template to a string' do
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
end
- end
- context 'with forked projects with submodules' do
- render_views
+ context 'with forked projects with submodules' do
+ render_views
- let(:project) { create(:project) }
- let(:fork_project) { create(:forked_project_with_submodules) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:forked_project_with_submodules) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
- before do
- fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
- fork_project.save
- merge_request.reload
- end
-
- it 'renders' do
- go format: 'json'
+ before do
+ fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ fork_project.save
+ merge_request.reload
+ go(format: 'json')
+ end
- expect(response).to be_success
- expect(response.body).to have_content('Subproject commit')
+ it 'renders' do
+ expect(response).to be_success
+ expect(response.body).to have_content('Subproject commit')
+ end
end
end
- end
- describe 'GET diffs with ignore_whitespace_change' do
- def go(format: 'html')
- get :diffs,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid,
- format: format,
- w: 1
- end
+ context 'with ignore_whitespace_change' do
+ context 'as html' do
+ before { go(format: 'html', w: 1) }
- context 'as html' do
- it 'renders the diff template' do
- go
+ it 'renders the diff template' do
+ expect(response).to render_template('diffs')
+ end
+ end
+
+ context 'as json' do
+ before { go(format: 'json', w: 1) }
- expect(response).to render_template('diffs')
+ it 'renders the diffs template to a string' do
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
end
end
- context 'as json' do
- it 'renders the diffs template to a string' do
- go format: 'json'
+ context 'with view' do
+ before { go(view: 'parallel') }
- expect(response).to render_template('projects/merge_requests/show/_diffs')
- expect(JSON.parse(response.body)).to have_key('html')
+ it 'saves the preferred diff view in a cookie' do
+ expect(response.cookies['diff_view']).to eq('parallel')
end
end
end
- describe 'GET diffs with view' do
- def go(extra_params = {})
+ describe 'GET diff_for_path' do
+ def diff_for_path(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- id: merge_request.iid
+ project_id: project.to_param
}
- get :diffs, params.merge(extra_params)
+ get :diff_for_path, params.merge(extra_params)
+ end
+
+ context 'when an ID param is passed' do
+ let(:existing_path) { 'files/ruby/popen.rb' }
+
+ context 'when the merge request exists' do
+ context 'when the user can view the merge request' do
+ context 'when the path exists in the diff' do
+ it 'enables diff notes' do
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+
+ expect(assigns(:diff_notes_disabled)).to be_falsey
+ expect(assigns(:comments_target)).to eq(noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id)
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(id: merge_request.iid, old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the user cannot view the merge request' do
+ before do
+ project.team.truncate
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'when the merge request does not exist' do
+ before { diff_for_path(id: merge_request.iid.succ, old_path: existing_path, new_path: existing_path) }
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when the merge request belongs to a different project' do
+ let(:other_project) { create(:empty_project) }
+
+ before do
+ other_project.team << [user, :master]
+ diff_for_path(id: merge_request.iid, old_path: existing_path, new_path: existing_path, project_id: other_project.to_param)
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
end
- it 'saves the preferred diff view in a cookie' do
- go view: 'parallel'
+ context 'when source and target params are passed' do
+ let(:existing_path) { 'files/ruby/feature.rb' }
+
+ context 'when both branches are in the same project' do
+ it 'disables diff notes' do
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_branch: 'feature', target_branch: 'master' })
+ end
+ end
+
+ context 'when the source branch is in a different project to the target' do
+ let(:other_project) { create(:project) }
+
+ before { other_project.team << [user, :master] }
+
+ context 'when the path exists in the diff' do
+ it 'disables diff notes' do
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
+
+ expect(assigns(:diff_notes_disabled)).to be_truthy
+ end
+
+ it 'only renders the diffs for the path given' do
+ expect(controller).to receive(:render_diff_for_path).and_wrap_original do |meth, diffs|
+ expect(diffs.diff_files.map(&:new_path)).to contain_exactly(existing_path)
+ meth.call(diffs)
+ end
+
+ diff_for_path(old_path: existing_path, new_path: existing_path, merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' })
+ end
+ end
+
+ context 'when the path does not exist in the diff' do
+ before { diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb', merge_request: { source_project: other_project, source_branch: 'feature', target_branch: 'master' }) }
- expect(response.cookies['diff_view']).to eq('parallel')
+ it 'returns a 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
@@ -409,4 +528,135 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+
+ context 'when the conflicts cannot be resolved in the UI' do
+ before do
+ allow_any_instance_of(Gitlab::Conflict::Parser).
+ to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'returns a 200 status code' do
+ expect(response).to have_http_status(:ok)
+ end
+
+ it 'returns JSON with a message' do
+ expect(json_response.keys).to contain_exactly('message', 'type')
+ end
+ end
+
+ context 'with valid conflicts' do
+ before do
+ get :conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json'
+ end
+
+ it 'includes meta info about the MR' do
+ expect(json_response['commit_message']).to include('Merge branch')
+ expect(json_response['commit_sha']).to match(/\h{40}/)
+ expect(json_response['source_branch']).to eq(merge_request_with_conflicts.source_branch)
+ expect(json_response['target_branch']).to eq(merge_request_with_conflicts.target_branch)
+ end
+
+ it 'includes each file that has conflicts' do
+ filenames = json_response['files'].map { |file| file['new_path'] }
+
+ expect(filenames).to contain_exactly('files/ruby/popen.rb', 'files/ruby/regex.rb')
+ end
+
+ it 'splits files into sections with lines' do
+ json_response['files'].each do |file|
+ file['sections'].each do |section|
+ expect(section).to include('conflict', 'lines')
+
+ section['lines'].each do |line|
+ if section['conflict']
+ expect(line['type']).to be_in(['old', 'new'])
+ expect(line.values_at('old_line', 'new_line')).to contain_exactly(nil, a_kind_of(Integer))
+ else
+ if line['type'].nil?
+ expect(line['old_line']).not_to eq(nil)
+ expect(line['new_line']).not_to eq(nil)
+ else
+ expect(line['type']).to eq('match')
+ expect(line['old_line']).to eq(nil)
+ expect(line['new_line']).to eq(nil)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ it 'has unique section IDs across files' do
+ section_ids = json_response['files'].flat_map do |file|
+ file['sections'].map { |section| section['id'] }.compact
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+ end
+ end
+
+ context 'POST resolve_conflicts' do
+ let(:json_response) { JSON.parse(response.body) }
+ let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
+
+ def resolve_conflicts(sections)
+ post :resolve_conflicts,
+ namespace_id: merge_request_with_conflicts.project.namespace.to_param,
+ project_id: merge_request_with_conflicts.project.to_param,
+ id: merge_request_with_conflicts.iid,
+ format: 'json',
+ sections: sections,
+ commit_message: 'Commit message'
+ end
+
+ context 'with valid params' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin')
+ end
+
+ it 'creates a new commit on the branch' do
+ expect(original_head_sha).not_to eq(merge_request_with_conflicts.source_branch_head.sha)
+ expect(merge_request_with_conflicts.source_branch_head.message).to include('Commit message')
+ end
+
+ it 'returns an OK response' do
+ expect(response).to have_http_status(:ok)
+ end
+ end
+
+ context 'when sections are missing' do
+ before do
+ resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head')
+ end
+
+ it 'returns a 400 error' do
+ expect(response).to have_http_status(:bad_request)
+ end
+
+ it 'has a message with the name of the first missing section' do
+ expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9')
+ end
+
+ it 'does not create a new commit' do
+ expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index d173bb350f1..4e3ef5dc6fa 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::MilestonesController do
end
describe "#destroy" do
- it "should remove milestone" do
+ it "removes milestone" do
expect(issue.milestone_id).to eq(milestone.id)
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 75590c1ed4f..92e38b02615 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,4 +1,4 @@
-require('spec_helper')
+require 'spec_helper'
describe Projects::NotesController do
let(:user) { create(:user) }
@@ -6,7 +6,15 @@ describe Projects::NotesController do
let(:issue) { create(:issue, project: project) }
let(:note) { create(:note, noteable: issue, project: project) }
- describe 'POST #toggle_award_emoji' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note
+ }
+ end
+
+ describe 'POST toggle_award_emoji' do
before do
sign_in(user)
project.team << [user, :developer]
@@ -14,23 +22,132 @@ describe Projects::NotesController do
it "toggles the award emoji" do
expect do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: note.id, name: "thumbsup")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { note.award_emoji.count }.by(1)
expect(response).to have_http_status(200)
end
it "removes the already awarded emoji" do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: note.id, name: "thumbsup")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
expect do
- post(:toggle_award_emoji, namespace_id: project.namespace.path,
- project_id: project.path, id: note.id, name: "thumbsup")
+ post(:toggle_award_emoji, request_params.merge(name: "thumbsup"))
end.to change { AwardEmoji.count }.by(-1)
expect(response).to have_http_status(200)
end
end
+
+ describe "resolving and unresolving" do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+
+ describe 'POST resolve' do
+ before do
+ sign_in user
+ end
+
+ context "when the user is not authorized to resolve the note" do
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the note" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the note is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the note is resolvable" do
+ it "resolves the note" do
+ post :resolve, request_params
+
+ expect(note.reload.resolved?).to be true
+ expect(note.reload.resolved_by).to eq(user)
+ end
+
+ it "sends notifications if all discussions are resolved" do
+ expect_any_instance_of(MergeRequests::ResolvedDiscussionNotificationService).to receive(:execute).with(merge_request)
+
+ post :resolve, request_params
+ end
+
+ it "returns the name of the resolving user" do
+ post :resolve, request_params
+
+ expect(JSON.parse(response.body)["resolved_by"]).to eq(user.name)
+ end
+
+ it "returns status 200" do
+ post :resolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE unresolve' do
+ before do
+ sign_in user
+
+ note.resolve!(user)
+ end
+
+ context "when the user is not authorized to resolve the note" do
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the user is authorized to resolve the note" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context "when the note is not resolvable" do
+ before do
+ note.update(system: true)
+ end
+
+ it "returns status 404" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context "when the note is resolvable" do
+ it "unresolves the note" do
+ delete :unresolve, request_params
+
+ expect(note.reload.resolved?).to be false
+ end
+
+ it "returns status 200" do
+ delete :unresolve, request_params
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 596d8d34b7c..da6112a13f7 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -3,7 +3,7 @@ require('spec_helper')
describe Projects::ProtectedBranchesController do
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
- it "redirect empty repo to projects page" do
+ it "redirects empty repo to projects page" do
get(:index, namespace_id: project.namespace.to_param, project_id: project.to_param)
end
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 48f799d8ca1..04bd9a01f7b 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -24,7 +24,7 @@ describe Projects::RawController do
context 'image header' do
let(:id) { 'master/files/images/6049019_460s.jpg' }
- it 'set image content type header' do
+ it 'sets image content type header' do
get(:show,
namespace_id: public_project.namespace.to_param,
project_id: public_project.to_param,
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index ccd8c741c83..cccd492ef06 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -19,7 +19,7 @@ describe Projects::ServicesController do
describe "#test" do
context 'success' do
- it "should redirect and show success message" do
+ it "redirects and show success message" do
expect(service).to receive(:test).and_return({ success: true, result: 'done' })
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
expect(response.status).to redirect_to('/')
@@ -28,7 +28,7 @@ describe Projects::ServicesController do
end
context 'failure' do
- it "should redirect and show failure message" do
+ it "redirects and show failure message" do
expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' })
get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
expect(response.status).to redirect_to('/')
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
new file mode 100644
index 00000000000..a6995145cc1
--- /dev/null
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::TagsController do
+ let(:project) { create(:project, :public) }
+ let!(:release) { create(:release, project: project) }
+ let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') }
+
+ describe 'GET index' do
+ before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param }
+
+ it 'returns the tags for the page' do
+ expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0'])
+ end
+
+ it 'returns releases matching those tags' do
+ expect(assigns(:releases)).to include(release)
+ expect(assigns(:releases)).not_to include(invalid_release)
+ end
+ end
+end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
new file mode 100644
index 00000000000..7b3a26d7ca7
--- /dev/null
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Projects::TemplatesController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:body) { JSON.parse(response.body) }
+
+ before do
+ project.team << [user, :developer]
+ sign_in(user)
+ end
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ end
+
+ describe '#show' do
+ it 'renders template name and content as json' do
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(200)
+ expect(body["name"]).to eq("bug")
+ expect(body["content"]).to eq("something valid")
+ end
+
+ it 'renders 404 when unauthorized' do
+ sign_in(user2)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "issue", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 when template type is not found' do
+ sign_in(user)
+ get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'renders 404 without errors' do
+ sign_in(user)
+ expect { get(:show, namespace_id: project.namespace.to_param, template_type: "dont_exist", key: "bug", project_id: project.path, format: :json) }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
index 5a8bba28594..936320a3709 100644
--- a/spec/controllers/projects/todo_controller_spec.rb
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -1,6 +1,8 @@
require('spec_helper')
describe Projects::TodosController do
+ include ApiHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
@@ -8,43 +10,51 @@ describe Projects::TodosController do
context 'Issues' do
describe 'POST create' do
+ def go
+ post :create,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue',
+ format: 'html'
+ end
+
context 'when authorized' do
before do
sign_in(user)
project.team << [user, :developer]
end
- it 'should create todo for issue' do
+ it 'creates todo for issue' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(1)
expect(response).to have_http_status(200)
end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ end
end
context 'when not authorized' do
- it 'should not create todo for issue that user has no access to' do
+ it 'does not create todo for issue that user has no access to' do
sign_in(user)
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(404)
end
- it 'should not create todo for issue when user not logged in' do
+ it 'does not create todo for issue when user not logged in' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: issue.id,
- issuable_type: 'issue')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(302)
@@ -55,43 +65,51 @@ describe Projects::TodosController do
context 'Merge Requests' do
describe 'POST create' do
+ def go
+ post :create,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request',
+ format: 'html'
+ end
+
context 'when authorized' do
before do
sign_in(user)
project.team << [user, :developer]
end
- it 'should create todo for merge request' do
+ it 'creates todo for merge request' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(1)
expect(response).to have_http_status(200)
end
+
+ it 'returns todo path and pending count' do
+ go
+
+ expect(response).to have_http_status(200)
+ expect(json_response['count']).to eq 1
+ expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ end
end
context 'when not authorized' do
- it 'should not create todo for merge request user has no access to' do
+ it 'does not create todo for merge request user has no access to' do
sign_in(user)
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(404)
end
- it 'should not create todo for merge request user has no access to' do
+ it 'does not create todo for merge request user has no access to' do
expect do
- post(:create, namespace_id: project.namespace.path,
- project_id: project.path,
- issuable_id: merge_request.id,
- issuable_type: 'merge_request')
+ go
end.to change { user.todos.count }.by(0)
expect(response).to have_http_status(302)
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 0893ee89f6a..71d0e4be834 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -14,9 +14,9 @@ describe Projects::UploadsController do
context "without params['file']" do
it "returns an error" do
- post :create,
+ post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
+ project_id: project.to_param,
format: :json
expect(response).to have_http_status(422)
end
@@ -34,23 +34,21 @@ describe Projects::UploadsController do
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
- expect(response.body).to match '\"is_image\":true'
end
end
context 'with valid non-image file' do
before do
- post :create,
+ post :create,
namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- file: txt,
+ project_id: project.to_param,
+ file: txt,
format: :json
end
it 'returns a content with original filename, new link, and correct type.' do
expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
expect(response.body).to match "\"url\":\"/uploads"
- expect(response.body).to match '\"is_image\":false'
end
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 1b1b1bdf52d..ffe0641ddd7 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -43,6 +43,26 @@ describe ProjectsController do
end
end
+ context "project with empty repo" do
+ let(:empty_project) { create(:project_empty_repo, :public) }
+
+ before { sign_in(user) }
+
+ User.project_views.keys.each do |project_view|
+ context "with #{project_view} view set" do
+ before do
+ user.update_attributes(project_view: project_view)
+
+ get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ end
+
+ it "renders the empty project view" do
+ expect(response).to render_template('empty')
+ end
+ end
+ end
+ end
+
context "rendering default project view" do
render_views
@@ -108,7 +128,7 @@ describe ProjectsController do
context "when the url contains .atom" do
let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') }
- it 'expect an error creating the project' do
+ it 'expects an error creating the project' do
expect(public_project_with_dot_atom).not_to be_valid
end
end
@@ -202,7 +222,7 @@ describe ProjectsController do
create(:forked_project_link, forked_to_project: project_fork)
end
- it 'should remove fork from project' do
+ it 'removes fork from project' do
delete(:remove_fork,
namespace_id: project_fork.namespace.to_param,
id: project_fork.to_param, format: :js)
@@ -216,7 +236,7 @@ describe ProjectsController do
context 'when project not forked' do
let(:unforked_project) { create(:project, namespace: user.namespace) }
- it 'should do nothing if project was not forked' do
+ it 'does nothing if project was not forked' do
delete(:remove_fork,
namespace_id: unforked_project.namespace.to_param,
id: unforked_project.to_param, format: :js)
@@ -236,7 +256,7 @@ describe ProjectsController do
end
describe "GET refs" do
- it "should get a list of branches and tags" do
+ it "gets a list of branches and tags" do
get :refs, namespace_id: public_project.namespace.path, id: public_project.path
parsed_body = JSON.parse(response.body)
@@ -245,7 +265,7 @@ describe ProjectsController do
expect(parsed_body["Commits"]).to be_nil
end
- it "should get a list of branches, tags and commits" do
+ it "gets a list of branches, tags and commits" do
get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
parsed_body = JSON.parse(response.body)
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
new file mode 100644
index 00000000000..35c4a0b6f08
--- /dev/null
+++ b/spec/factories/boards.rb
@@ -0,0 +1,5 @@
+FactoryGirl.define do
+ factory :board do
+ project factory: :empty_project
+ end
+end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index efe9803b1a7..c2fdf89213a 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -1,8 +1,8 @@
FactoryGirl.define do
factory :broadcast_message do
message "MyText"
- starts_at Date.yesterday
- ends_at Date.tomorrow
+ starts_at 1.day.ago
+ ends_at 1.day.from_now
trait :expired do
starts_at 5.days.ago
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index fe05a0cfc00..0c93bbdfe26 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -3,8 +3,11 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :ci_build, class: Ci::Build do
name 'test'
+ stage 'test'
+ stage_idx 0
ref 'master'
tag false
+ status 'pending'
created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
@@ -15,6 +18,11 @@ FactoryGirl.define do
services: ["postgres"]
}
end
+ yaml_variables do
+ [
+ { key: :DB_NAME, value: 'postgres', public: true }
+ ]
+ end
pipeline factory: :ci_pipeline
@@ -38,6 +46,15 @@ FactoryGirl.define do
status 'pending'
end
+ trait :created do
+ status 'created'
+ end
+
+ trait :manual do
+ status 'skipped'
+ self.when 'manual'
+ end
+
trait :allowed_to_fail do
allow_failure true
end
@@ -78,5 +95,21 @@ FactoryGirl.define do
build.save!
end
end
+
+ trait :artifacts_expired do
+ after(:create) do |build, _|
+ build.artifacts_file =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
+ 'application/zip')
+
+ build.artifacts_metadata =
+ fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
+ 'application/x-gzip')
+
+ build.artifacts_expire_at = 1.minute.ago
+
+ build.save!
+ end
+ end
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c..ac2a1ba5dff 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -1,24 +1,8 @@
-# == Schema Information
-#
-# Table name: commits
-#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
-# gl_project_id :integer
-#
-
FactoryGirl.define do
factory :ci_empty_pipeline, class: Ci::Pipeline do
+ ref 'master'
sha '97de212e80737a608d939f648d959671fb0a0142'
+ status 'pending'
project factory: :empty_project
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index 6d47d05f8ad..b8d8fab0e0b 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -5,7 +5,8 @@ FactoryGirl.define do
variables do
{
- TRIGGER_KEY: 'TRIGGER_VALUE'
+ TRIGGER_KEY_1: 'TRIGGER_VALUE_1',
+ TRIGGER_KEY_2: 'TRIGGER_VALUE_2'
}
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 1e5c479616c..995f2080f10 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -7,6 +7,30 @@ FactoryGirl.define do
started_at 'Tue, 26 Jan 2016 08:21:42 +0100'
finished_at 'Tue, 26 Jan 2016 08:23:42 +0100'
+ trait :success do
+ status 'success'
+ end
+
+ trait :failed do
+ status 'failed'
+ end
+
+ trait :canceled do
+ status 'canceled'
+ end
+
+ trait :running do
+ status 'running'
+ end
+
+ trait :pending do
+ status 'pending'
+ end
+
+ trait :created do
+ status 'created'
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 07265c26ca3..846cccfc7fa 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -3,5 +3,6 @@ FactoryGirl.define do
sequence(:name) { |n| "environment#{n}" }
project factory: :empty_project
+ sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" }
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index e72aa9479b7..2c0a2dd94ca 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -18,5 +18,15 @@ FactoryGirl.define do
factory :closed_issue, traits: [:closed]
factory :reopened_issue, traits: [:reopened]
+
+ factory :labeled_issue do
+ transient do
+ labels []
+ end
+
+ after(:create) do |issue, evaluator|
+ issue.update_attributes(labels: evaluator.labels)
+ end
+ end
end
end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
new file mode 100644
index 00000000000..9e3f06c682c
--- /dev/null
+++ b/spec/factories/lists.rb
@@ -0,0 +1,20 @@
+FactoryGirl.define do
+ factory :list do
+ board
+ label
+ list_type :label
+ sequence(:position)
+ end
+
+ factory :backlog_list, parent: :list do
+ list_type :backlog
+ label nil
+ position nil
+ end
+
+ factory :done_list, parent: :list do
+ list_type :done
+ label nil
+ position nil
+ end
+end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 3195fb3ddcc..424ecc65759 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -5,5 +5,16 @@ FactoryGirl.define do
trait :token do
token { SecureRandom.hex(10) }
end
+
+ trait :all_events_enabled do
+ push_events true
+ merge_requests_events true
+ tag_push_events true
+ issues_events true
+ note_events true
+ build_events true
+ pipeline_events true
+ wiki_page_events true
+ end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index b682ced75ac..f82d68a1816 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -83,4 +83,10 @@ FactoryGirl.define do
)
end
end
+
+ factory :project_with_board, parent: :empty_project do
+ after(:create) do |project|
+ project.create_board
+ end
+ end
end
diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb
index 28ed8078157..b2695e0482a 100644
--- a/spec/factories/protected_branches.rb
+++ b/spec/factories/protected_branches.rb
@@ -2,5 +2,28 @@ FactoryGirl.define do
factory :protected_branch do
name
project
+
+ after(:build) do |protected_branch|
+ protected_branch.push_access_levels.new(access_level: Gitlab::Access::MASTER)
+ protected_branch.merge_access_levels.new(access_level: Gitlab::Access::MASTER)
+ end
+
+ trait :developers_can_push do
+ after(:create) do |protected_branch|
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ trait :developers_can_merge do
+ after(:create) do |protected_branch|
+ protected_branch.merge_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
+ end
+ end
+
+ trait :no_one_can_push do
+ after(:create) do |protected_branch|
+ protected_branch.push_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
+ end
+ end
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 7fc20cd5555..866e663f026 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -23,6 +23,10 @@ FactoryGirl.define do
action { Todo::BUILD_FAILED }
end
+ trait :approval_required do
+ action { Todo::APPROVAL_REQUIRED }
+ end
+
trait :done do
state :done
end
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
new file mode 100644
index 00000000000..9763cc0cf15
--- /dev/null
+++ b/spec/factories/user_agent_details.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user_agent_detail do
+ ip_address '127.0.0.1'
+ user_agent 'AppleWebKit/537.36'
+ association :subject, factory: :issue
+ end
+end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index 675d9bd18b7..786e1456f5f 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -9,7 +9,7 @@ describe 'factories' do
expect { entity }.not_to raise_error
end
- it 'should be valid', if: factory.build_class < ActiveRecord::Base do
+ it 'is valid', if: factory.build_class < ActiveRecord::Base do
expect(entity).to be_valid
end
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 16baf7e9516..c1731e6414a 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -11,7 +11,7 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
describe 'in the abuse report view' do
- it "should present a link to the user's profile" do
+ it "presents a link to the user's profile" do
visit admin_abuse_reports_path
expect(page).to have_link user.name, href: user_path(user)
@@ -19,7 +19,7 @@ describe "Admin::AbuseReports", feature: true, js: true do
end
describe 'in the profile page of the user' do
- it 'should show a link to the admin view of the user' do
+ it 'shows a link to the admin view of the user' do
visit user_path(user)
expect(page).to have_link '', href: admin_user_path(user)
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index a6198389f04..e177059d959 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -36,12 +36,45 @@ describe 'Admin Builds' do
end
end
+ context 'Pending tab' do
+ context 'when have pending builds' do
+ it 'shows pending builds' do
+ build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build2 = create(:ci_build, pipeline: pipeline, status: :running)
+ build3 = create(:ci_build, pipeline: pipeline, status: :success)
+ build4 = create(:ci_build, pipeline: pipeline, status: :failed)
+
+ visit admin_builds_path(scope: :pending)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Pending')
+ expect(page.find('.build-link')).to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page.find('.build-link')).not_to have_content(build4.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+
+ context 'when have no builds pending' do
+ it 'shows a message' do
+ create(:ci_build, pipeline: pipeline, status: :success)
+
+ visit admin_builds_path(scope: :pending)
+
+ expect(page).to have_selector('.nav-links li.active', text: 'Pending')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
+ end
+
context 'Running tab' do
context 'when have running builds' do
it 'shows running builds' do
- build1 = create(:ci_build, pipeline: pipeline, status: :pending)
+ build1 = create(:ci_build, pipeline: pipeline, status: :running)
build2 = create(:ci_build, pipeline: pipeline, status: :success)
build3 = create(:ci_build, pipeline: pipeline, status: :failed)
+ build4 = create(:ci_build, pipeline: pipeline, status: :pending)
visit admin_builds_path(scope: :running)
@@ -49,6 +82,7 @@ describe 'Admin Builds' do
expect(page.find('.build-link')).to have_content(build1.id)
expect(page.find('.build-link')).not_to have_content(build2.id)
expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page.find('.build-link')).not_to have_content(build4.id)
expect(page).to have_link 'Cancel all'
end
end
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 5b1c0460274..66044b44495 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -45,7 +45,6 @@ feature 'Admin disables Git access protocol', feature: true do
expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
expect(page).to have_selector('#clone-dropdown')
end
-
end
def visit_project
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 7964951ae99..b3ce72b1452 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -9,7 +9,7 @@ describe "Admin::Hooks", feature: true do
end
describe "GET /admin/hooks" do
- it "should be ok" do
+ it "is ok" do
visit admin_root_path
page.within ".layout-nav" do
@@ -19,7 +19,7 @@ describe "Admin::Hooks", feature: true do
expect(current_path).to eq(admin_hooks_path)
end
- it "should have hooks list" do
+ it "has hooks list" do
visit admin_hooks_path
expect(page).to have_content(@system_hook.url)
end
@@ -33,7 +33,7 @@ describe "Admin::Hooks", feature: true do
expect { click_button "Add System Hook" }.to change(SystemHook, :count).by(1)
end
- it "should open new hook popup" do
+ it "opens new hook popup" do
expect(current_path).to eq(admin_hooks_path)
expect(page).to have_content(@url)
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 101d955d693..30ded9202a4 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -11,11 +11,11 @@ describe "Admin::Projects", feature: true do
visit admin_namespaces_projects_path
end
- it "should be ok" do
+ it "is ok" do
expect(current_path).to eq(admin_namespaces_projects_path)
end
- it "should have projects list" do
+ it "has projects list" do
expect(page).to have_content(@project.name)
end
end
@@ -26,7 +26,7 @@ describe "Admin::Projects", feature: true do
click_link "#{@project.name}"
end
- it "should have project info" do
+ it "has project info" do
expect(page).to have_content(@project.path)
expect(page).to have_content(@project.name)
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index f4e5c26b519..1df972843e2 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -6,12 +6,49 @@ describe 'Admin System Info' do
end
describe 'GET /admin/system_info' do
- it 'shows system info page' do
- visit admin_system_info_path
+ let(:cpu) { double(:cpu, length: 2) }
+ let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) }
- expect(page).to have_content 'CPU'
- expect(page).to have_content 'Memory'
- expect(page).to have_content 'Disks'
+ context 'when all info is available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when CPU info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU Unable to collect CPU info'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when memory info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory Unable to collect memory info'
+ expect(page).to have_content 'Disks'
+ end
end
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 767504df251..cb3191dfdde 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Users", feature: true do
visit admin_users_path
end
- it "should be ok" do
+ it "is ok" do
expect(current_path).to eq(admin_users_path)
end
- it "should have users list" do
+ it "has users list" do
expect(page).to have_content(@user.email)
expect(page).to have_content(@user.name)
end
@@ -66,11 +66,11 @@ describe "Admin::Users", feature: true do
fill_in "user_email", with: "bigbang@mail.com"
end
- it "should create new user" do
+ it "creates new user" do
expect { click_button "Create user" }.to change {User.count}.by(1)
end
- it "should apply defaults to user" do
+ it "applies defaults to user" do
click_button "Create user"
user = User.find_by(username: 'bang')
expect(user.projects_limit).
@@ -79,20 +79,20 @@ describe "Admin::Users", feature: true do
to eq(Gitlab.config.gitlab.default_can_create_group)
end
- it "should create user with valid data" do
+ it "creates user with valid data" do
click_button "Create user"
user = User.find_by(username: 'bang')
expect(user.name).to eq('Big Bang')
expect(user.email).to eq('bigbang@mail.com')
end
- it "should call send mail" do
+ it "calls send mail" do
expect_any_instance_of(NotificationService).to receive(:new_user)
click_button "Create user"
end
- it "should send valid email to user with email & password" do
+ it "sends valid email to user with email & password" do
perform_enqueued_jobs do
click_button "Create user"
end
@@ -106,7 +106,7 @@ describe "Admin::Users", feature: true do
end
describe "GET /admin/users/:id" do
- it "should have user info" do
+ it "has user info" do
visit admin_users_path
click_link @user.name
@@ -123,13 +123,13 @@ describe "Admin::Users", feature: true do
expect(page).to have_content('Impersonate')
end
- it 'should not show impersonate button for admin itself' do
+ it 'does not show impersonate button for admin itself' do
visit admin_user_path(@user)
expect(page).not_to have_content('Impersonate')
end
- it 'should not show impersonate button for blocked user' do
+ it 'does not show impersonate button for blocked user' do
another_user.block
visit admin_user_path(another_user)
@@ -153,7 +153,7 @@ describe "Admin::Users", feature: true do
expect(icon).not_to eql nil
end
- it 'can log out of impersonated user back to original user' do
+ it 'logs out of impersonated user back to original user' do
find(:css, 'li.impersonation a').click
expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username)
@@ -197,7 +197,7 @@ describe "Admin::Users", feature: true do
click_link "edit_user_#{@simple_user.id}"
end
- it "should have user edit page" do
+ it "has user edit page" do
expect(page).to have_content('Name')
expect(page).to have_content('Password')
end
@@ -212,12 +212,12 @@ describe "Admin::Users", feature: true do
click_button "Save changes"
end
- it "should show page with new data" do
+ it "shows page with new data" do
expect(page).to have_content('bigbang@mail.com')
expect(page).to have_content('Big Bang')
end
- it "should change user entry" do
+ it "changes user entry" do
@simple_user.reload
expect(@simple_user.name).to eq('Big Bang')
expect(@simple_user.is_admin?).to be_truthy
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index f81a3c117ff..746df36bb25 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -5,7 +5,7 @@ describe "Dashboard Feed", feature: true do
let!(:user) { create(:user, name: "Jonh") }
context "projects atom feed via private token" do
- it "should render projects atom feed" do
+ it "renders projects atom feed" do
visit dashboard_projects_path(:atom, private_token: user.private_token)
expect(body).to have_selector('feed title')
end
@@ -23,11 +23,11 @@ describe "Dashboard Feed", feature: true do
visit dashboard_projects_path(:atom, private_token: user.private_token)
end
- it "should have issue opened event" do
+ it "has issue opened event" do
expect(body).to have_content("#{user.name} opened issue ##{issue.iid}")
end
- it "should have issue comment event" do
+ it "has issue comment event" do
expect(body).
to have_content("#{user.name} commented on issue ##{issue.iid}")
end
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index baa7814e96a..09c140868fb 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -9,7 +9,7 @@ describe 'Issues Feed', feature: true do
before { project.team << [user, :developer] }
context 'when authenticated' do
- it 'should render atom feed' do
+ it 'renders atom feed' do
login_with user
visit namespace_project_issues_path(project.namespace, project, :atom)
@@ -22,7 +22,7 @@ describe 'Issues Feed', feature: true do
end
context 'when authenticated via private token' do
- it 'should render atom feed' do
+ it 'renders atom feed' do
visit namespace_project_issues_path(project.namespace, project, :atom,
private_token: user.private_token)
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 91704377a07..a8833194421 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -5,7 +5,7 @@ describe "User Feed", feature: true do
let!(:user) { create(:user) }
context 'user atom feed via private token' do
- it "should render user atom feed" do
+ it "renders user atom feed" do
visit user_path(user, :atom, private_token: user.private_token)
expect(body).to have_selector('feed title')
end
@@ -43,24 +43,24 @@ describe "User Feed", feature: true do
visit user_path(user, :atom, private_token: user.private_token)
end
- it 'should have issue opened event' do
+ it 'has issue opened event' do
expect(body).to have_content("#{safe_name} opened issue ##{issue.iid}")
end
- it 'should have issue comment event' do
+ it 'has issue comment event' do
expect(body).
to have_content("#{safe_name} commented on issue ##{issue.iid}")
end
- it 'should have XHTML summaries in issue descriptions' do
+ it 'has XHTML summaries in issue descriptions' do
expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
end
- it 'should have XHTML summaries in notes' do
+ it 'has XHTML summaries in notes' do
expect(body).to match /Bug confirmed <img[^>]*\/>/
end
- it 'should have XHTML summaries in merge request descriptions' do
+ it 'has XHTML summaries in merge request descriptions' do
expect(body).to match /Here is the fix: <\/p><div[^>]*><a[^>]*><img[^>]*\/><\/a><\/div>/
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
new file mode 100644
index 00000000000..5d777895542
--- /dev/null
+++ b/spec/features/boards/boards_spec.rb
@@ -0,0 +1,634 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+
+ before do
+ project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
+
+ project.team << [user, :master]
+ project.team << [user2, :master]
+
+ login_as(user)
+ end
+
+ context 'no lists' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'shows blank state' do
+ expect(page).to have_content('Welcome to your Issue Board!')
+ end
+
+ it 'hides the blank state when clicking nevermind button' do
+ page.within(find('.board-blank-state')) do
+ click_button("Nevermind, I'll use my own")
+ end
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'creates default lists' do
+ lists = ['Backlog', 'Development', 'Testing', 'Production', 'Ready', 'Done']
+
+ page.within(find('.board-blank-state')) do
+ click_button('Add default lists')
+ end
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 6)
+
+ page.all('.board').each_with_index do |list, i|
+ expect(list.find('.board-title')).to have_content(lists[i])
+ end
+ end
+ end
+
+ context 'with lists' do
+ let(:milestone) { create(:milestone, project: project) }
+
+ let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let!(:backlog) { create(:label, project: project, name: 'Backlog') }
+ let!(:done) { create(:label, project: project, name: 'Done') }
+
+ let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
+ let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
+
+ let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+ let!(:issue1) { create(:issue, project: project, assignee: user) }
+ let!(:issue2) { create(:issue, project: project, author: user2) }
+ let!(:issue3) { create(:issue, project: project) }
+ let!(:issue4) { create(:issue, project: project) }
+ let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
+ let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
+ let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:issue8) { create(:closed_issue, project: project) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 4)
+ expect(find('.board:nth-child(1)')).to have_selector('.card')
+ expect(find('.board:nth-child(2)')).to have_selector('.card')
+ expect(find('.board:nth-child(3)')).to have_selector('.card')
+ expect(find('.board:nth-child(4)')).to have_selector('.card')
+ end
+
+ it 'shows lists' do
+ expect(page).to have_selector('.board', count: 4)
+ end
+
+ it 'shows issues in lists' do
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+
+ page.within(find('.board:nth-child(3)')) do
+ expect(page.find('.board-header')).to have_content('2')
+ expect(page).to have_selector('.card', count: 2)
+ end
+ end
+
+ it 'shows confidential issues with icon' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.confidential-icon', count: 1)
+ end
+ end
+
+ it 'allows user to delete board' do
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'removes checkmark in new list dropdown after deleting' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within(find('.board:nth-child(2)')) do
+ find('.board-delete').click
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active')
+ end
+
+ it 'infinite scrolls list' do
+ 50.times do
+ create(:issue, project: project)
+ end
+
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+ wait_for_vue_resource(spinner: false)
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ context 'backlog' do
+ it 'shows issues in backlog with no labels' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'is searchable' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'clears search' do
+ page.within(find('.board', match: :first)) do
+ find('.form-control').set issue1.title
+
+ expect(page).to have_selector('.card', count: 1)
+
+ find('.board-search-clear-btn').click
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ end
+ end
+
+ it 'moves issue from backlog into list' do
+ drag_to(list_to_index: 1)
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('3')
+ expect(page).to have_selector('.card', count: 3)
+ end
+ end
+ end
+
+ context 'done' do
+ it 'shows list of done issues' do
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ end
+
+ it 'moves issue to done' do
+ drag_to(list_from_index: 0, list_to_index: 3)
+
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2)
+ expect(find('.board:nth-child(4)')).to have_content(issue9.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+
+ it 'removes all of the same issue to done' do
+ drag_to(list_from_index: 1, list_to_index: 3)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(4)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(4)')).not_to have_content(planning.title)
+ end
+ end
+
+ context 'lists' do
+ it 'changes position of list' do
+ drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header')
+
+ expect(find('.board:nth-child(2)')).to have_content(development.title)
+ expect(find('.board:nth-child(2)')).to have_content(planning.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 1, card_index: 1, list_to_index: 2)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_content(issue6.title)
+ expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ end
+
+ it 'issue moves between lists' do
+ drag_to(list_from_index: 2, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_content(issue7.title)
+ expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ end
+
+ it 'issue moves from done' do
+ drag_to(list_from_index: 3, list_to_index: 1)
+
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 3)
+ expect(find('.board:nth-child(2)')).to have_content(issue8.title)
+ end
+
+ context 'issue card' do
+ it 'shows assignee' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.avatar', count: 1)
+ end
+ end
+ end
+
+ context 'new list' do
+ it 'shows all labels in new list dropdown' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ expect(page).to have_content(planning.title)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(testing.title)
+ end
+ end
+
+ it 'creates new list for label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Backlog label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link backlog.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'creates new list for Done label' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link done.title
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 5)
+ end
+
+ it 'moves issues from backlog into new list' do
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('6')
+ expect(page).to have_selector('.card', count: 6)
+ end
+
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link testing.title
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+ end
+ end
+ end
+
+ context 'filtering' do
+ it 'filters by author' do
+ page.within '.issues-filters' do
+ click_button('Author')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-author' do
+ click_link(user2.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-author-search')).to have_content(user2.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by assignee' do
+ page.within '.issues-filters' do
+ click_button('Assignee')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-assignee' do
+ click_link(user.name)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-assignee-search')).to have_content(user.name)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by milestone' do
+ page.within '.issues-filters' do
+ click_button('Milestone')
+ wait_for_ajax
+
+ page.within '.milestone-filter' do
+ click_link(milestone.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(find('.js-milestone-select')).to have_content(milestone.title)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+ end
+
+ it 'filters by label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'infinite scrolls list with label filter' do
+ 50.times do
+ create(:labeled_issue, project: project, labels: [testing])
+ end
+
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('20')
+ expect(page).to have_selector('.card', count: 20)
+
+ evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight")
+
+ expect(page.find('.board-header')).to have_content('40')
+ expect(page).to have_selector('.card', count: 40)
+ end
+ end
+
+ it 'filters by multiple labels' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within(find('.dropdown-menu-labels')) do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ click_link(bug.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by no label' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link("No Label")
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('5')
+ expect(page).to have_selector('.card', count: 5)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'filters by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ expect(page).to have_selector('.card', count: 6)
+ expect(find('.card', match: :first)).to have_content(bug.title)
+ click_button(bug.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+
+ it 'removes label filter by clicking label button on issue' do
+ page.within(find('.board', match: :first)) do
+ page.within(find('.card', match: :first)) do
+ click_button(bug.title)
+ end
+ wait_for_vue_resource(spinner: false)
+
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ wait_for_vue_resource
+
+ page.within('.labels-filter') do
+ expect(find('.dropdown-toggle-text')).to have_content(bug.title)
+ end
+ end
+ end
+ end
+
+ context 'keyboard shortcuts' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'allows user to use keyboard shortcuts' do
+ find('.boards-list').native.send_keys('i')
+ expect(page).to have_content('New Issue')
+ end
+ end
+
+ context 'signed out user' do
+ before do
+ logout
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ context 'as guest user' do
+ let(:user_guest) { create(:user) }
+
+ before do
+ project.team << [user_guest, :guest]
+ logout
+ login_as(user_guest)
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not show create new list' do
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+ end
+
+ def drag_to(list_from_index: 0, card_index: 0, to_index: 0, list_to_index: 0, selector: '.board-list')
+ evaluate_script("simulateDrag({scrollable: document.getElementById('board-app'), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{card_index}}, to: {el: $('.board-list').eq(#{list_to_index}).get(0), index: #{to_index}}});")
+
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ end
+
+ wait_for_vue_resource
+ end
+
+ def wait_for_vue_resource(spinner: true)
+ Timeout.timeout(Capybara.default_max_wait_time) do
+ loop until page.evaluate_script('Vue.activeResources').zero?
+ end
+
+ if spinner
+ expect(find('.boards-list')).not_to have_selector('.fa-spinner')
+ end
+ end
+end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 16832c297ac..0cfeb2e57d8 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -13,17 +13,33 @@ describe "Builds" do
end
describe "GET /:project/builds" do
+ context "Pending scope" do
+ before do
+ 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
+ end
+ end
+
context "Running scope" do
before do
@build.run!
visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'Running') }
- it { expect(page).to have_link 'Cancel running' }
- it { expect(page).to have_content @build.short_sha }
- it { expect(page).to have_content @build.ref }
- it { expect(page).to have_content @build.name }
+ 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
+ end
end
context "Finished scope" do
@@ -32,9 +48,11 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') }
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to have_link 'Cancel running' }
+ it "shows Finished tab builds" do
+ expect(page).to have_selector('.nav-links li.active', text: 'Finished')
+ expect(page).to have_content 'No builds to show'
+ expect(page).to have_link 'Cancel running'
+ end
end
context "All builds" do
@@ -43,11 +61,13 @@ describe "Builds" do
visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_selector('.nav-links li.active', text: 'All') }
- it { expect(page).to have_content @build.short_sha }
- it { expect(page).to have_content @build.ref }
- it { expect(page).to have_content @build.name }
- it { expect(page).not_to have_link 'Cancel running' }
+ 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).not_to have_link 'Cancel running'
+ end
end
end
@@ -179,9 +199,13 @@ describe "Builds" do
click_link 'Retry'
end
- it { expect(page.status_code).to eq(200) }
- it { expect(page).to have_content 'pending' }
- it { expect(page).to have_content 'Cancel' }
+ it 'shows the right status and buttons' do
+ expect(page).to have_http_status(200)
+ expect(page).to have_content 'pending'
+ page.within('aside.right-sidebar') do
+ expect(page).to have_content 'Cancel'
+ end
+ end
end
context "Build from other project" do
@@ -192,7 +216,25 @@ describe "Builds" do
page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2))
end
- it { expect(page.status_code).to eq(404) }
+ it { expect(page).to have_http_status(404) }
+ end
+
+ context "Build that current user is not allowed to retry" do
+ before do
+ @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)
+ end
+
+ it 'does not show the Retry button' do
+ page.within('aside.right-sidebar') do
+ expect(page).not_to have_content 'Retry'
+ end
+ end
end
end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index 30e29d9d552..81077f4b005 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -17,7 +17,7 @@ describe 'CI Lint' do
File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
end
- it 'Yaml parsing' do
+ it 'parses Yaml' do
within "table" do
expect(page).to have_content('Job - rspec')
expect(page).to have_content('Job - spinach')
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 45e1a157a1f..5910803df51 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -52,7 +52,7 @@ describe 'Commits' do
visit namespace_project_commits_path(project.namespace, project, :master)
end
- it 'should show build status' do
+ it 'shows build status' do
page.within("//li[@id='commit-#{pipeline.short_sha}']") do
expect(page).to have_css(".ci-status-link")
end
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
new file mode 100644
index 00000000000..ca7f73e24cc
--- /dev/null
+++ b/spec/features/compare_spec.rb
@@ -0,0 +1,42 @@
+require "spec_helper"
+
+describe "Compare", js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_compare_index_path(project.namespace, project, from: "master", to: "master")
+ end
+
+ describe "branches" do
+ it "pre-populates fields" do
+ expect(page.find_field("from").value).to eq("master")
+ end
+
+ it "compares branches" do
+ fill_in "from", with: "fea"
+ find("#from").click
+
+ click_link "feature"
+ expect(page.find_field("from").value).to eq("feature")
+
+ click_button "Compare"
+ expect(page).to have_content "Commits"
+ end
+ end
+
+ describe "tags" do
+ it "compares tags" do
+ fill_in "from", with: "v1.0"
+ find("#from").click
+
+ click_link "v1.0.0"
+ expect(page.find_field("from").value).to eq("v1.0.0")
+
+ click_button "Compare"
+ expect(page).to have_content "Commits"
+ end
+ end
+end
diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb
index 24e83d44010..4cff12de854 100644
--- a/spec/features/dashboard/label_filter_spec.rb
+++ b/spec/features/dashboard/label_filter_spec.rb
@@ -16,7 +16,7 @@ describe 'Dashboard > label filter', feature: true, js: true do
end
context 'duplicate labels' do
- it 'should remove duplicate labels' do
+ it 'removes duplicate labels' do
page.within('.labels-filter') do
click_button 'Label'
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 39805da9d0b..3fb1cb37544 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -16,7 +16,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
visit_issues
end
- it 'should show all issues with no milestone' do
+ it 'shows all issues with no milestone' do
show_milestone_dropdown
click_link 'No Milestone'
@@ -24,7 +24,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_selector('.issue', count: 1)
end
- it 'should show all issues with any milestone' do
+ it 'shows all issues with any milestone' do
show_milestone_dropdown
click_link 'Any Milestone'
@@ -32,7 +32,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
expect(page).to have_selector('.issue', count: 2)
end
- it 'should show all issues with the selected milestone' do
+ it 'shows all issues with the selected milestone' do
show_milestone_dropdown
page.within '.dropdown-content' do
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 7fb28f4174b..fcd41b38413 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -13,6 +13,7 @@ feature 'Environments', feature: true do
describe 'when showing environments' do
given!(:environment) { }
given!(:deployment) { }
+ given!(:manual) { }
before do
visit namespace_project_environments_path(project.namespace, project)
@@ -43,6 +44,24 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
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
+ end
end
end
@@ -54,6 +73,7 @@ feature 'Environments', feature: true do
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)
@@ -72,20 +92,36 @@ feature 'Environments', feature: true do
expect(page).to have_link(deployment.short_sha)
end
- scenario 'does not show a retry button for deployment without build' do
- expect(page).not_to have_link('Retry')
+ scenario 'does not show a re-deploy button for deployment without build' do
+ expect(page).not_to have_link('Re-deploy')
end
context 'with build' do
- given(:build) { create(:ci_build, project: project) }
+ 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})")
end
- scenario 'does show retry button' do
- expect(page).to have_link('Retry')
+ scenario 'does show re-deploy button' do
+ expect(page).to have_link('Re-deploy')
+ 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
end
end
end
@@ -104,7 +140,7 @@ feature 'Environments', feature: true do
context 'for valid name' do
before do
fill_in('Name', with: 'production')
- click_on 'Create environment'
+ click_on 'Save'
end
scenario 'does create a new pipeline' do
@@ -115,7 +151,7 @@ feature 'Environments', feature: true do
context 'for invalid name' do
before do
fill_in('Name', with: 'name with spaces')
- click_on 'Create environment'
+ click_on 'Save'
end
scenario 'does show errors' do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
new file mode 100644
index 00000000000..688f68d3cff
--- /dev/null
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -0,0 +1,248 @@
+require 'spec_helper'
+
+feature 'Expand and collapse diffs', js: true, feature: true do
+ include WaitForAjax
+
+ let(:branch) { 'expand-collapse-diffs' }
+
+ before do
+ login_as :admin
+ project = create(:project)
+
+ # Ensure that undiffable.md is in .gitattributes
+ project.repository.copy_gitattributes(branch)
+ visit namespace_project_commit_path(project.namespace, project, project.commit(branch))
+ execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
+ end
+
+ def file_container(filename)
+ find("[data-blob-diff-path*='#{filename}']")
+ end
+
+ # Use define_method instead of let (which is memoized) so that this just works across a
+ # reload.
+ #
+ files = [
+ 'small_diff.md', 'large_diff.md', 'large_diff_renamed.md', 'undiffable.md',
+ 'too_large.md', 'too_large_image.jpg'
+ ]
+
+ files.each do |file|
+ define_method(file.split('.').first) { file_container(file) }
+ end
+
+ context 'visiting a commit with collapsed diffs' do
+ it 'shows small diffs immediately' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'collapses large diffs by default' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+ end
+
+ it 'collapses large diffs for renamed files by default' do
+ expect(large_diff_renamed).not_to have_selector('.code')
+ expect(large_diff_renamed).to have_selector('.nothing-here-block')
+ expect(large_diff_renamed).to have_selector('.file-title .deletion')
+ expect(large_diff_renamed).to have_selector('.file-title .addition')
+ end
+
+ it 'shows non-renderable diffs as such immediately, regardless of their size' do
+ expect(undiffable).not_to have_selector('.code')
+ expect(undiffable).to have_selector('.nothing-here-block')
+ expect(undiffable).to have_content('gitattributes')
+ end
+
+ it 'does not allow diffs that are larger than the maximum size to be expanded' do
+ expect(too_large).not_to have_selector('.code')
+ expect(too_large).to have_selector('.nothing-here-block')
+ expect(too_large).to have_content('too large')
+ end
+
+ it 'shows image diffs immediately, regardless of their size' do
+ expect(too_large_image).not_to have_selector('.nothing-here-block')
+ expect(too_large_image).to have_selector('.image')
+ end
+
+ context 'expanding a diff for a renamed file' do
+ before do
+ large_diff_renamed.find('.nothing-here-block').click
+ wait_for_ajax
+ end
+
+ it 'shows the old content' do
+ old_line = large_diff_renamed.find('.line_content.old')
+
+ expect(old_line).to have_content('two copies')
+ end
+
+ it 'shows the new content' do
+ new_line = large_diff_renamed.find('.line_content.new', match: :prefer_exact)
+
+ expect(new_line).to have_content('three copies')
+ end
+ end
+
+ context 'expanding a large diff' do
+ before do
+ click_link('large_diff.md')
+ wait_for_ajax
+ end
+
+ it 'makes a request to get the content' do
+ ajax_uris = evaluate_script('ajaxUris')
+
+ expect(ajax_uris).not_to be_empty
+ expect(ajax_uris.first).to include('large_diff.md')
+ end
+
+ it 'shows the diff content' do
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ context 'adding a comment to the expanded diff' do
+ let(:comment_text) { 'A comment' }
+
+ before do
+ large_diff.find('.diff-line-num', match: :prefer_exact).hover
+ large_diff.find('.add-diff-note').click
+ large_diff.find('.note-textarea').send_keys comment_text
+ large_diff.find_button('Comment').click
+ wait_for_ajax
+ end
+
+ it 'adds the comment' do
+ expect(large_diff.find('.notes')).to have_content comment_text
+ end
+
+ context 'reloading the page' do
+ before { refresh }
+
+ it 'collapses the large diff by default' do
+ expect(large_diff).not_to have_selector('.code')
+ expect(large_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 'expanding the diff' do
+ before do
+ click_link('large_diff.md')
+ wait_for_ajax
+ end
+
+ it 'shows the diff content' do
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'shows the diff comment' do
+ expect(large_diff.find('.notes')).to have_content comment_text
+ end
+ end
+ end
+ end
+ end
+
+ context 'collapsing an expanded diff' do
+ before { click_link('small_diff.md') }
+
+ it 'hides the diff content' do
+ expect(small_diff).not_to have_selector('.code')
+ expect(small_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 're-expanding the same diff' do
+ before { click_link('small_diff.md') }
+
+ it 'shows the diff content' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'does not make a new HTTP request' do
+ expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md'))
+ end
+ end
+ end
+ end
+
+ context 'visiting a commit without collapsed diffs' do
+ let(:branch) { 'feature' }
+
+ it 'does not show Expand all button' do
+ expect(page).not_to have_link('Expand all')
+ end
+ end
+
+ context 'visiting a commit with more than safe files' do
+ let(:branch) { 'expand-collapse-files' }
+
+ # safe-files -> 100 | safe-lines -> 5000 | commit-files -> 105
+ it 'does collapsing from the safe number of files to the end on small files' do
+ expect(page).to have_link('Expand all')
+
+ expect(page).to have_selector('.diff-content', count: 105)
+ expect(page).to have_selector('.diff-collapsed', count: 5)
+
+ %w(file-95.txt file-96.txt file-97.txt file-98.txt file-99.txt).each do |filename|
+ expect(find("[data-blob-diff-path*='#{filename}']")).to have_selector('.diff-collapsed')
+ end
+ end
+ end
+
+ context 'visiting a commit with more than safe lines' do
+ let(:branch) { 'expand-collapse-lines' }
+
+ # safe-files -> 100 | safe-lines -> 5000 | commit_files -> 8 (each 1250 lines)
+ it 'does collapsing from the safe number of lines to the end' do
+ expect(page).to have_link('Expand all')
+
+ expect(page).to have_selector('.diff-content', count: 6)
+ expect(page).to have_selector('.diff-collapsed', count: 2)
+
+ %w(file-4.txt file-5.txt).each do |filename|
+ expect(find("[data-blob-diff-path*='#{filename}']")).to have_selector('.diff-collapsed')
+ end
+ end
+ end
+
+ context 'expanding all diffs' do
+ before do
+ click_link('Expand all')
+ wait_for_ajax
+ execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
+ end
+
+ it 'reloads the page with all diffs expanded' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+
+ expect(large_diff).to have_selector('.code')
+ expect(large_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ context 'collapsing an expanded diff' do
+ before { click_link('small_diff.md') }
+
+ it 'hides the diff content' do
+ expect(small_diff).not_to have_selector('.code')
+ expect(small_diff).to have_selector('.nothing-here-block')
+ end
+
+ context 're-expanding the same diff' do
+ before { click_link('small_diff.md') }
+
+ it 'shows the diff content' do
+ expect(small_diff).to have_selector('.code')
+ expect(small_diff).not_to have_selector('.nothing-here-block')
+ end
+
+ it 'does not make a new HTTP request' do
+ expect(evaluate_script('ajaxUris')).not_to include(a_string_matching('small_diff.md'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index a89ac09f236..84d73d693bc 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -23,25 +23,25 @@ describe "GitLab Flavored Markdown", feature: true do
end
describe "for commits" do
- it "should render title in commits#index" do
+ it "renders title in commits#index" do
visit namespace_project_commits_path(project.namespace, project, 'master', limit: 1)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in commits#show" do
+ it "renders title in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link(issue.to_reference)
end
- it "should render description in commits#show" do
+ it "renders description in commits#show" do
visit namespace_project_commit_path(project.namespace, project, commit)
expect(page).to have_link(fred.to_reference)
end
- it "should render title in repositories#branches" do
+ it "renders title in repositories#branches" do
visit namespace_project_branches_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
@@ -62,19 +62,19 @@ describe "GitLab Flavored Markdown", feature: true do
description: "ask #{fred.to_reference} for details")
end
- it "should render subject in issues#index" do
+ it "renders subject in issues#index" do
visit namespace_project_issues_path(project.namespace, project)
expect(page).to have_link(@other_issue.to_reference)
end
- it "should render subject in issues#show" do
+ it "renders subject in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link(@other_issue.to_reference)
end
- it "should render details in issues#show" do
+ it "renders details in issues#show" do
visit namespace_project_issue_path(project.namespace, project, @issue)
expect(page).to have_link(fred.to_reference)
@@ -86,13 +86,13 @@ describe "GitLab Flavored Markdown", feature: true do
@merge_request = create(:merge_request, source_project: project, target_project: project, title: "fix #{issue.to_reference}")
end
- it "should render title in merge_requests#index" do
+ it "renders title in merge_requests#index" do
visit namespace_project_merge_requests_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in merge_requests#show" do
+ it "renders title in merge_requests#show" do
visit namespace_project_merge_request_path(project.namespace, project, @merge_request)
expect(page).to have_link(issue.to_reference)
@@ -107,19 +107,19 @@ describe "GitLab Flavored Markdown", feature: true do
description: "ask #{fred.to_reference} for details")
end
- it "should render title in milestones#index" do
+ it "renders title in milestones#index" do
visit namespace_project_milestones_path(project.namespace, project)
expect(page).to have_link(issue.to_reference)
end
- it "should render title in milestones#show" do
+ it "renders title in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link(issue.to_reference)
end
- it "should render description in milestones#show" do
+ it "renders description in milestones#show" do
visit namespace_project_milestone_path(project.namespace, project, @milestone)
expect(page).to have_link(fred.to_reference)
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index d1a6a98ab72..b3baa2ab57c 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -12,6 +12,13 @@ feature 'Groups > Members > User requests access', feature: true do
visit group_path(group)
end
+ scenario 'request access feature is disabled' do
+ group.update_attributes(request_access_enabled: false)
+ visit group_path(group)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
scenario 'user can request access to a group' do
perform_enqueued_jobs { click_link 'Request Access' }
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 891df65216d..2d8b59472e8 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -1,14 +1,26 @@
require 'spec_helper'
feature 'Group', feature: true do
+ before do
+ login_as(:admin)
+ end
+
+ describe 'creating a group with space in group path' do
+ it 'renders new group form with validation errors' do
+ visit new_group_path
+ fill_in 'Group path', with: 'space group'
+
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.")
+ end
+ end
+
describe 'description' do
let(:group) { create(:group) }
let(:path) { group_path(group) }
- before do
- login_as(:admin)
- end
-
it 'parses Markdown' do
group.update_attribute(:description, 'This is **my** group')
visit path
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 8c6b669ce78..e2101b333e2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -5,8 +5,8 @@ describe 'Help Pages', feature: true do
before do
login_as :user
end
- it 'replace the variable $your_email with the email of the user' do
- visit help_page_path('ssh', 'README')
+ 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}\"")
end
end
diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb
new file mode 100644
index 00000000000..9a2b879e789
--- /dev/null
+++ b/spec/features/issuables/default_sort_order_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe 'Projects > Issuables > Default sort order', feature: true do
+ let(:project) { create(:empty_project, :public) }
+
+ let(:first_created_issuable) { issuables.order_created_asc.first }
+ let(:last_created_issuable) { issuables.order_created_desc.first }
+
+ let(:first_updated_issuable) { issuables.order_updated_asc.first }
+ let(:last_updated_issuable) { issuables.order_updated_desc.first }
+
+ context 'for merge requests' do
+ include MergeRequestHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ source_branch: "#{issuable_type}_#{i}",
+ source_project: project }.merge(ts)
+ end
+
+ MergeRequest.all
+ end
+
+ context 'in the "merge requests" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests project
+
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / open" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests_with_state(project, 'open')
+
+ expect(selected_sort_order).to eq('last created')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / merged" tab', js: true do
+ let(:issuable_type) { :merged_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'merged')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / closed" tab', js: true do
+ let(:issuable_type) { :closed_merge_request }
+
+ it 'is "last updated"' do
+ visit_merge_requests_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "merge requests / all" tab', js: true do
+ let(:issuable_type) { :merge_request }
+
+ it 'is "last created"' do
+ visit_merge_requests_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_merge_request).to include(last_created_issuable.title)
+ expect(last_merge_request).to include(first_created_issuable.title)
+ end
+ end
+ end
+
+ context 'for issues' do
+ include IssueHelpers
+
+ let!(:issuables) do
+ timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago },
+ { created_at: 2.minutes.ago, updated_at: 30.seconds.ago },
+ { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }]
+
+ timestamps.each_with_index do |ts, i|
+ create issuable_type, { title: "#{issuable_type}_#{i}",
+ project: project }.merge(ts)
+ end
+
+ Issue.all
+ end
+
+ context 'in the "issues" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues project
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / open" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues_with_state(project, 'open')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'in the "issues / closed" tab', js: true do
+ let(:issuable_type) { :closed_issue }
+
+ it 'is "last updated"' do
+ visit_issues_with_state(project, 'closed')
+
+ expect(find('.issues-other-filters')).to have_content('Last updated')
+ expect(first_issue).to include(last_updated_issuable.title)
+ expect(last_issue).to include(first_updated_issuable.title)
+ end
+ end
+
+ context 'in the "issues / all" tab', js: true do
+ let(:issuable_type) { :issue }
+
+ it 'is "last created"' do
+ visit_issues_with_state(project, 'all')
+
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_desc' do
+ let(:issuable_type) { :issue }
+
+ before { visit_issues(project, sort: 'id_desc') }
+
+ it 'shows the sort order as last created' do
+ expect(find('.issues-other-filters')).to have_content('Last created')
+ expect(first_issue).to include(last_created_issuable.title)
+ expect(last_issue).to include(first_created_issuable.title)
+ end
+ end
+
+ context 'when the sort in the URL is id_asc' do
+ let(:issuable_type) { :issue }
+
+ before { visit_issues(project, sort: 'id_asc') }
+
+ it 'shows the sort order as oldest created' do
+ expect(find('.issues-other-filters')).to have_content('Oldest created')
+ expect(first_issue).to include(first_created_issuable.title)
+ expect(last_issue).to include(last_created_issuable.title)
+ end
+ end
+ end
+
+ def selected_sort_order
+ find('.pull-right .dropdown button').text.downcase
+ end
+
+ def visit_merge_requests_with_state(project, state)
+ visit_merge_requests project
+ visit_issuables_with_state state
+ end
+
+ def visit_issues_with_state(project, state)
+ visit_issues project
+ visit_issuables_with_state state
+ end
+
+ def visit_issuables_with_state(state)
+ within('.issues-state-filters') { find("span", text: state.titleize).click }
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 07a854ea014..6eb04cf74c5 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -21,32 +21,32 @@ describe 'Awards Emoji', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should increment the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
- it 'should increment the thumbsup emoji', js: true do
+ it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click
sleep 2
expect(thumbsup_emoji).to have_text("1")
end
- it 'should decrement the thumbsdown emoji', js: true do
+ it 'decrements the thumbsdown emoji', js: true do
expect(thumbsdown_emoji).to have_text("0")
end
end
context 'click the thumbsdown emoji' do
- it 'should increment the thumbsdown emoji', js: true do
+ it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click
sleep 2
expect(thumbsdown_emoji).to have_text("1")
end
- it 'should decrement the thumbsup emoji', js: true do
+ it 'decrements the thumbsup emoji', js: true do
expect(thumbsup_emoji).to have_text("0")
end
end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
index 63efecf8780..401e1ea2b89 100644
--- a/spec/features/issues/award_spec.rb
+++ b/spec/features/issues/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Issue awards', js: true, feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should add award to issue' do
+ it 'adds award to issue' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
@@ -20,7 +20,7 @@ feature 'Issue awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '1'
end
- it 'should remove award from issue' do
+ it 'removes award from issue' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
@@ -29,7 +29,7 @@ feature 'Issue awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '0'
end
- it 'should only have one menu on the page' do
+ it 'only has one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
@@ -42,7 +42,7 @@ feature 'Issue awards', js: true, feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should not see award menu button' do
+ it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index afc093cc1f5..bc2c087c9b9 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -175,7 +175,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'labels are kept' do
+ it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
@@ -197,7 +197,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'existing label is kept and new label is present' do
+ it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
check 'check_all_issues'
@@ -222,7 +222,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'existing label is kept and new label is present' do
+ it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
@@ -252,7 +252,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'labels are kept' do
+ it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue1.id}")).to have_content 'First Release'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 5ea02b8d39c..908b18e5339 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -37,25 +37,25 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix1" and "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1" in issues list' do
+ it 'does not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1"
end
- it 'should show label "bug" in filtered-labels' do
+ it 'shows label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
end
- it 'should not show label "feature" and "enhancement" in filtered-labels' do
+ it 'does not show label "feature" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
- it 'should remove label "bug"' do
+ it 'removes label "bug"' do
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
@@ -71,20 +71,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Feature1" in issues list' do
+ it 'shows issue "Feature1" in issues list' do
expect(page).to have_content "Feature1"
end
- it 'should not show "Bugfix1" and "Bugfix2" in issues list' do
+ it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
end
- it 'should show label "feature" in filtered-labels' do
+ it 'shows label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature"
end
- it 'should not show label "bug" and "enhancement" in filtered-labels' do
+ it 'does not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
@@ -99,20 +99,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1" and "Bugfix1" in issues list' do
+ it 'does not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
end
- it 'should show label "enhancement" in filtered-labels' do
+ it 'shows label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
end
- it 'should not show label "feature" and "bug" in filtered-labels' do
+ it 'does not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end
@@ -128,21 +128,21 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should not show "Bugfix1" or "Feature1" in issues list' do
+ it 'does not show "Bugfix1" or "Feature1" in issues list' do
expect(page).not_to have_content "Bugfix1"
expect(page).not_to have_content "Feature1"
end
- it 'should show label "enhancement" and "feature" in filtered-labels' do
+ it 'shows label "enhancement" and "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
expect(find('.filtered-labels')).to have_content "feature"
end
- it 'should not show label "bug" in filtered-labels' do
+ it 'does not show label "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
end
- it 'should remove label "enhancement"' do
+ it 'removes label "enhancement"' do
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
expect(find('.filtered-labels')).to have_no_content "enhancement"
@@ -159,20 +159,20 @@ feature 'Issue filtering by Labels', feature: true do
wait_for_ajax
end
- it 'should show issue "Bugfix2" in issues list' do
+ it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
end
- it 'should not show "Feature1"' do
+ it 'does not show "Feature1"' do
expect(page).not_to have_content "Feature1"
end
- it 'should show label "bug" and "enhancement" in filtered-labels' do
+ it 'shows label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
end
- it 'should not show label "feature" in filtered-labels' do
+ it 'does not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
@@ -191,7 +191,7 @@ feature 'Issue filtering by Labels', feature: true do
end
end
- it 'should allow user to remove filtered labels' do
+ it 'allows user to remove filtered labels' do
first('.js-label-filter-remove').click
wait_for_ajax
@@ -201,11 +201,11 @@ feature 'Issue filtering by Labels', feature: true do
end
context 'dropdown filtering', js: true do
- it 'should filter by label name' do
+ it 'filters by label name' do
page.within '.labels-filter' do
click_button 'Label'
wait_for_ajax
- fill_in 'label-name', with: 'bug'
+ find('.dropdown-input input').set 'bug'
page.within '.dropdown-content' do
expect(page).not_to have_content 'enhancement'
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 99445185893..485dc560061 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -15,7 +15,7 @@ feature 'Issue filtering by Milestone', feature: true do
end
context 'filters by upcoming milestone', js: true do
- it 'should not show issues with no expiry' do
+ it 'does not show issues with no expiry' do
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
@@ -25,7 +25,7 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 0)
end
- it 'should show issues in future' do
+ it 'shows issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
@@ -36,7 +36,7 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 1)
end
- it 'should not show issues in past' do
+ it 'does not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:issue, project: project)
create(:issue, project: project, milestone: milestone)
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 4b9b5394b61..e262f285868 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -26,17 +26,17 @@ describe 'Filter issues', feature: true do
end
context 'assignee', js: true do
- it 'should update to current user' do
+ it 'updates to current user' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -56,17 +56,17 @@ describe 'Filter issues', feature: true do
end
context 'milestone', js: true do
- it 'should update to current milestone' do
+ it 'updates to current milestone' do
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-milestone-select .dropdown-toggle-text')).to have_content(milestone.title)
@@ -81,7 +81,7 @@ describe 'Filter issues', feature: true do
wait_for_ajax
end
- it 'should filter by any label' do
+ it 'filters by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
@@ -89,7 +89,7 @@ describe 'Filter issues', feature: true do
expect(find('.labels-filter')).to have_content 'Label'
end
- it 'should filter by no label' do
+ it 'filters by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
wait_for_ajax
@@ -100,7 +100,7 @@ describe 'Filter issues', feature: true do
expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
end
- it 'should filter by no label' do
+ it 'filters by no label' do
find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
expect(page).to have_content label.title
@@ -117,7 +117,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-user-link', text: user.username).click
- wait_for_ajax
+ expect(page).not_to have_selector('.issues-list .issue')
find('.js-label-select').click
@@ -128,19 +128,19 @@ describe 'Filter issues', feature: true do
end
context 'assignee and label', js: true do
- it 'should update to current assignee and label' do
+ it 'updates to current assignee and label' do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'should not change when closed link is clicked' do
+ it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
expect(find('.js-label-select .dropdown-toggle-text')).to have_content(label.title)
end
- it 'should not change when all link is clicked' do
+ it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
@@ -168,7 +168,7 @@ describe 'Filter issues', feature: true do
end
context 'only text', js: true do
- it 'should filter issues by searched text' do
+ it 'filters issues by searched text' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -176,7 +176,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should not show any issues' do
+ it 'does not show any issues' do
fill_in 'issue_search', with: 'testing'
page.within '.issues-list' do
@@ -186,7 +186,7 @@ describe 'Filter issues', feature: true do
end
context 'text and dropdown options', js: true do
- it 'should filter by text and label' do
+ it 'filters by text and label' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -204,7 +204,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and milestone' do
+ it 'filters by text and milestone' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -221,7 +221,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and assignee' do
+ it 'filters by text and assignee' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -238,7 +238,7 @@ describe 'Filter issues', feature: true do
end
end
- it 'should filter by text and author' do
+ it 'filters by text and author' do
fill_in 'issue_search', with: 'Bug'
page.within '.issues-list' do
@@ -269,7 +269,7 @@ describe 'Filter issues', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- it 'should be able to filter and sort issues' do
+ it 'is able to filter and sort issues' do
click_button 'Label'
wait_for_ajax
page.within '.labels-filter' do
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 5739bc64dfb..4b1aec8bf71 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -17,7 +17,7 @@ feature 'Issue Sidebar', feature: true do
end
describe 'when clicking on edit labels', js: true do
- it 'dropdown has an option to create a new label' do
+ it 'shows dropdown option to create a new label' do
find('.block.labels .edit-link').click
page.within('.block.labels') do
@@ -27,7 +27,7 @@ feature 'Issue Sidebar', feature: true do
end
context 'creating a new label', js: true do
- it 'option to crate a new label is present' do
+ it 'shows option to crate a new label is present' do
page.within('.block.labels') do
find('.edit-link').click
@@ -35,7 +35,7 @@ feature 'Issue Sidebar', feature: true do
end
end
- it 'dropdown switches to "create label" section' do
+ it 'shows dropdown switches to "create label" section' do
page.within('.block.labels') do
find('.edit-link').click
click_link 'Create new'
@@ -44,7 +44,7 @@ feature 'Issue Sidebar', feature: true do
end
end
- it 'new label is added' do
+ it 'adds new label' do
page.within('.block.labels') do
find('.edit-link').click
sleep 1
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index 16e188d2a8a..fb0c4704285 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do
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}")
+ note: "Mentioned in !#{referenced_mr.iid}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
@@ -41,7 +41,7 @@ feature 'Start new branch from an issue', feature: true do
end
context "for visiters" do
- it 'no button is shown', js: true do
+ it 'shows no buttons', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
expect(page).not_to have_css('#new-branch')
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index bc0f437a8ce..de8fdda388d 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -11,7 +11,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should create todo when clicking button' do
+ it 'creates todo when clicking button' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
expect(page).to have_content 'Mark Done'
@@ -28,7 +28,7 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
end
end
- it 'should mark a todo as done' do
+ it 'marks a todo as done' do
page.within '.issuable-sidebar' do
click_button 'Add Todo'
click_button 'Mark Done'
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index ddbd69b2891..ae5da3877a8 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -13,7 +13,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
context 'status', js: true do
- it 'should be set to closed' do
+ it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -24,7 +24,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(page).to have_selector('.issue', count: 0)
end
- it 'should be set to open' do
+ it 'sets to open' do
create_closed
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
@@ -38,7 +38,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
context 'assignee', js: true do
- it 'should update to current user' do
+ it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -52,7 +52,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
end
- it 'should update to unassigned' do
+ it 'updates to unassigned' do
create_assigned
visit namespace_project_issues_path(project.namespace, project)
@@ -68,7 +68,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
context 'milestone', js: true do
let(:milestone) { create(:milestone, project: project) }
- it 'should update milestone' do
+ it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
@@ -80,7 +80,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(find('.issue')).to have_content milestone.title
end
- it 'should set to no milestone' do
+ it 'sets to no milestone' do
create_with_milestone
visit namespace_project_issues_path(project.namespace, project)
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
new file mode 100644
index 00000000000..2883e392694
--- /dev/null
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -0,0 +1,58 @@
+require 'rails_helper'
+
+feature 'Issues > User uses slash commands', feature: true, js: true do
+ include WaitForAjax
+
+ it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
+ let(:issuable) { create(:issue, project: project) }
+ end
+
+ describe 'issue-only commands' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ describe 'adding a due date from note' do
+ let(:issue) { create(:issue, project: project) }
+
+ it 'does not create a note, and sets the due date accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/due 2016-08-28"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ end
+ end
+
+ describe 'removing a due date from note' do
+ let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
+
+ it 'does not create a note, and removes the due date accordingly' do
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/remove_due_date"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index d51c9abea19..d744d0eb6af 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Issues', feature: true do
+ include IssueHelpers
include SortingHelper
let(:project) { create(:project) }
@@ -25,7 +26,7 @@ describe 'Issues', feature: true do
find('.js-zen-enter').click
end
- it 'should open new issue popup' do
+ it 'opens new issue popup' do
expect(page).to have_content("Issue ##{issue.iid}")
end
@@ -70,7 +71,7 @@ describe 'Issues', feature: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
- it 'should save with due date' do
+ it 'saves with due date' do
date = Date.today.at_beginning_of_month
fill_in 'issue_title', with: 'bug 345'
@@ -98,7 +99,7 @@ describe 'Issues', feature: true do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should save with due date' do
+ it 'saves with due date' do
date = Date.today.at_beginning_of_month
expect(find('#issuable-due-date').value).to eq date.to_s
@@ -121,6 +122,17 @@ describe 'Issues', feature: true do
expect(page).to have_content date.to_s(:medium)
end
end
+
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
end
@@ -154,7 +166,7 @@ describe 'Issues', feature: true do
let(:issue) { @issue }
- it 'should allow filtering by issues with no specified assignee' do
+ it 'allows filtering by issues with no specified assignee' do
visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE)
expect(page).to have_content 'foobar'
@@ -162,7 +174,7 @@ describe 'Issues', feature: true do
expect(page).not_to have_content 'gitlab'
end
- it 'should allow filtering by a specified assignee' do
+ it 'allows filtering by a specified assignee' do
visit namespace_project_issues_path(project.namespace, project, assignee_id: @user.id)
expect(page).not_to have_content 'foobar'
@@ -186,15 +198,15 @@ describe 'Issues', feature: true do
it 'sorts by newest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created)
- expect(first_issue).to include('baz')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
end
it 'sorts by oldest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
+ expect(first_issue).to include('baz')
+ expect(last_issue).to include('foo')
end
it 'sorts by most recently updated' do
@@ -350,8 +362,8 @@ describe 'Issues', feature: true do
sort: sort_value_oldest_created,
assignee_id: user2.id)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('bar')
+ expect(first_issue).to include('bar')
+ expect(last_issue).to include('foo')
expect(page).not_to have_content 'baz'
end
end
@@ -513,7 +525,7 @@ describe 'Issues', feature: true do
visit new_namespace_project_issue_path(project.namespace, project)
end
- it 'should upload file when dragging into textarea' do
+ it 'uploads file when dragging into textarea' do
drop_in_dropzone test_image_file
# Wait for the file to upload
@@ -524,6 +536,35 @@ describe 'Issues', feature: true do
end
end
+ xdescribe 'new issue by email' do
+ shared_examples 'show the email in the modal' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+
+ visit namespace_project_issues_path(project.namespace, project)
+ click_button('Email a new issue')
+ end
+
+ it 'click the button to show modal for the new email' do
+ page.within '#issue-email-modal' do
+ email = project.new_issue_address(@user)
+
+ expect(page).to have_selector("input[value='#{email}']")
+ end
+ end
+ end
+
+ context 'with existing issues' do
+ let!(:issue) { create(:issue, project: project, author: @user) }
+
+ it_behaves_like 'show the email in the modal'
+ end
+
+ context 'without existing issues' do
+ it_behaves_like 'show the email in the modal'
+ end
+ end
+
describe 'due date' do
context 'update due on issue#show', js: true do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
@@ -532,7 +573,7 @@ describe 'Issues', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'should add due date to issue' do
+ it 'adds due date to issue' do
page.within '.due_date' do
click_link 'Edit'
@@ -544,7 +585,7 @@ describe 'Issues', feature: true do
end
end
- it 'should remove due date from issue' do
+ it 'removes due date from issue' do
page.within '.due_date' do
click_link 'Edit'
@@ -561,14 +602,6 @@ describe 'Issues', feature: true do
end
end
- def first_issue
- page.all('ul.issues-list > li').first.text
- end
-
- def last_issue
- page.all('ul.issues-list > li').last.text
- end
-
def drop_in_dropzone(file_path)
# Generate a fake input selector
page.execute_script <<-JS
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 72b5ff231f7..2523b4b7898 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -28,6 +28,11 @@ feature 'Login', feature: true do
end
describe 'with two-factor authentication' do
+ def enter_code(code)
+ fill_in 'Two-Factor Authentication code', with: code
+ click_button 'Verify code'
+ end
+
context 'with valid username/password' do
let(:user) { create(:user, :two_factor) }
@@ -36,11 +41,6 @@ feature 'Login', feature: true do
expect(page).to have_content('Two-Factor Authentication')
end
- def enter_code(code)
- fill_in 'Two-Factor Authentication code', with: code
- click_button 'Verify code'
- end
-
it 'does not show a "You are already signed in." error message' do
enter_code(user.current_otp)
expect(page).not_to have_content('You are already signed in.')
@@ -108,6 +108,39 @@ feature 'Login', feature: true do
end
end
end
+
+ context 'logging in via OAuth' do
+ def saml_config
+ OpenStruct.new(name: 'saml', label: 'saml', args: {
+ assertion_consumer_service_url: 'https://localhost:3443/users/auth/saml/callback',
+ idp_cert_fingerprint: '26:43:2C:47:AF:F0:6B:D0:07:9C:AD:A3:74:FE:5D:94:5F:4E:9E:52',
+ idp_sso_target_url: 'https://idp.example.com/sso/saml',
+ issuer: 'https://localhost:3443/',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ })
+ end
+
+ def stub_omniauth_config(messages)
+ Rails.application.env_config['devise.mapping'] = Devise.mappings[:user]
+ Rails.application.routes.disable_clear_and_finalize = true
+ Rails.application.routes.draw do
+ post '/users/auth/saml' => 'omniauth_callbacks#saml'
+ end
+ allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: saml_config)
+ allow(Gitlab.config.omniauth).to receive_messages(messages)
+ expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
+ end
+
+ it 'shows 2FA prompt after OAuth login' do
+ stub_omniauth_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], providers: [saml_config])
+ user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: 'saml')
+ login_via('saml', user, 'my-uid')
+
+ expect(page).to have_content('Two-Factor Authentication')
+ enter_code(user.current_otp)
+ expect(current_path).to eq root_path
+ end
+ end
end
describe 'without two-factor authentication' do
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 09ccc77c101..32159559c37 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -236,6 +236,14 @@ describe 'GitLab Markdown', feature: true do
it 'includes TaskListFilter' do
expect(doc).to parse_task_lists
end
+
+ it 'includes InlineDiffFilter' do
+ expect(doc).to parse_inline_diffs
+ end
+
+ it 'includes VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ end
end
context 'wiki pipeline' do
@@ -293,6 +301,10 @@ describe 'GitLab Markdown', feature: true do
it 'includes InlineDiffFilter' do
expect(doc).to parse_inline_diffs
end
+
+ it 'includes VideoLinkFilter' do
+ expect(doc).to parse_video_links
+ end
end
# Fake a `current_user` helper
diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb
index 007f67d6080..ac260e118d0 100644
--- a/spec/features/merge_requests/award_spec.rb
+++ b/spec/features/merge_requests/award_spec.rb
@@ -11,7 +11,7 @@ feature 'Merge request awards', js: true, feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'should add award to merge request' do
+ it 'adds award to merge request' do
first('.js-emoji-btn').click
expect(page).to have_selector('.js-emoji-btn.active')
expect(first('.js-emoji-btn')).to have_content '1'
@@ -20,7 +20,7 @@ feature 'Merge request awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '1'
end
- it 'should remove award from merge request' do
+ it 'removes award from merge request' do
first('.js-emoji-btn').click
find('.js-emoji-btn.active').click
expect(first('.js-emoji-btn')).to have_content '0'
@@ -29,7 +29,7 @@ feature 'Merge request awards', js: true, feature: true do
expect(first('.js-emoji-btn')).to have_content '0'
end
- it 'should only have one menu on the page' do
+ it 'has only one menu on the page' do
first('.js-add-award').click
expect(page).to have_selector('.emoji-menu')
@@ -42,7 +42,7 @@ feature 'Merge request awards', js: true, feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
- it 'should not see award menu button' do
+ it 'does not see award menu button' do
expect(page).not_to have_selector('.js-award-holder')
end
end
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
new file mode 100644
index 00000000000..759edf8ec80
--- /dev/null
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+feature 'Merge request conflict resolution', js: true, feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start', source_project: project) do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ context 'when a merge request can be resolved in the UI' do
+ let(:merge_request) { create_merge_request('conflict-resolvable') }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'shows a link to the conflict resolution page' do
+ expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ context 'visiting the conflicts resolution page' do
+ before { click_link('conflicts', href: /\/conflicts\Z/) }
+
+ it 'shows the conflicts' do
+ begin
+ expect(find('#conflicts')).to have_content('popen.rb')
+ rescue Capybara::Poltergeist::JavascriptError
+ retry
+ end
+ end
+ end
+ end
+
+ UNRESOLVABLE_CONFLICTS = {
+ 'conflict-too-large' => 'when the conflicts contain a large file',
+ 'conflict-binary-file' => 'when the conflicts contain a binary file',
+ 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
+ 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
+ }
+
+ UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
+ context description do
+ let(:merge_request) { create_merge_request(source_branch) }
+
+ before do
+ project.team << [user, :developer]
+ login_as(user)
+
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not show a link to the conflict resolution page' do
+ expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+ end
+
+ it 'shows an error if the conflicts page is visited directly' do
+ visit current_url + '/conflicts'
+ wait_for_ajax
+
+ expect(find('#conflicts')).to have_content('Please try to resolve them locally.')
+ end
+ end
+ 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 e296078bad8..b963d1305b5 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -8,11 +8,14 @@ feature 'Create New Merge Request', feature: true, js: true do
project.team << [user, :master]
login_as user
- visit namespace_project_merge_requests_path(project.namespace, project)
end
it 'generates a diff for an orphaned branch' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
click_link 'New Merge Request'
+ expect(page).to have_content('Source branch')
+ expect(page).to have_content('Target branch')
first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content a', text: 'orphaned-branch').click
@@ -40,4 +43,20 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_content private_project.to_reference
end
end
+
+ it 'allows to change the diff view' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'master', source_branch: 'fix' })
+
+ click_link 'Changes'
+
+ expect(page).to have_css('a.btn.active', text: 'Inline')
+ expect(page).not_to have_css('a.btn.active', text: 'Side-by-side')
+
+ click_link 'Side-by-side'
+
+ within '.merge-request' do
+ expect(page).not_to have_css('a.btn.active', text: 'Inline')
+ expect(page).to have_css('a.btn.active', text: 'Side-by-side')
+ end
+ end
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index f676200ecf3..4d5d4aa121a 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -29,12 +29,16 @@ feature 'Merge request created from fork' do
include WaitForAjax
given(:pipeline) do
- create(:ci_pipeline_with_two_job, project: fork_project,
- sha: merge_request.diff_head_sha,
- ref: merge_request.source_branch)
+ create(:ci_pipeline,
+ project: fork_project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
end
- background { pipeline.create_builds(user) }
+ background do
+ create(:ci_build, pipeline: pipeline, name: 'rspec')
+ create(:ci_build, pipeline: pipeline, name: 'spinach')
+ end
scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request)
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
new file mode 100644
index 00000000000..c6adf7e4c56
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -0,0 +1,497 @@
+require 'spec_helper'
+
+feature 'Diff notes resolve', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ context 'no discussions' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ note.destroy
+ visit_merge_request
+ end
+
+ it 'displays no discussion resolved data' do
+ expect(page).not_to have_content('discussion resolved')
+ expect(page).not_to have_selector('.discussion-next-btn')
+ end
+ end
+
+ context 'as authorized user' do
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit_merge_request
+ end
+
+ context 'single discussion' do
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ 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 '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to mark discussion as resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ 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
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'hides resolved discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ visit_merge_request
+
+ expect(page).to have_selector('.discussion-body', visible: false)
+ end
+
+ it 'allows user to resolve from reply form without a comment' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve from reply form without a comment' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ sleep 1
+
+ click_button 'Reply...'
+
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ expect(page).not_to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & resolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to quickly scroll to next unresolved discussion' do
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ end
+
+ it 'hides jump to next button when all resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ expect(page).to have_selector('.discussion-next-btn', visible: false)
+ end
+
+ it 'updates updated text after resolving note' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content("Resolved by #{user.name}")
+ end
+
+ it 'hides jump to next discussion button' do
+ page.within '.discussion-reply-holder' do
+ expect(page).not_to have_selector('.discussion-next-btn')
+ end
+ end
+ end
+
+ context 'multiple notes' do
+ before do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ end
+
+ it 'does not mark discussion as resolved when resolving single note' do
+ page.within '.diff-content .note' do
+ first('.line-resolve-btn').click
+ sleep 1
+ expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
+ end
+
+ expect(page).to have_content('Last updated')
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'resolves discussion' do
+ page.all('.note').each do |note|
+ note.find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content('Resolved by')
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ end
+ end
+ end
+
+ context 'muliple discussions' do
+ before do
+ create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request)
+ visit_merge_request
+ end
+
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/2 discussions resolved')
+ end
+ end
+
+ it 'allows user to mark a single note as resolved' do
+ click_button('Resolve discussion', match: :first)
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/2 discussions resolved')
+ end
+ end
+
+ it 'allows user to mark all notes as resolved' do
+ page.all('.line-resolve-btn').each do |btn|
+ btn.click
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user user to mark all discussions as resolved' do
+ page.all('.discussion-reply-holder').each do |reply_holder|
+ page.within reply_holder do
+ click_button 'Resolve discussion'
+ end
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('2/2 discussions resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to quickly scroll to next unresolved discussion' do
+ page.within first('.discussion-reply-holder') do
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(page.evaluate_script("$('body').scrollTop()")).to be > 0
+ end
+
+ it 'updates updated text after resolving note' do
+ page.within first('.diff-content .note') do
+ find('.line-resolve-btn').click
+ end
+
+ expect(page).to have_content("Resolved by #{user.name}")
+ end
+
+ it 'shows jump to next discussion button' do
+ page.all('.discussion-reply-holder').each do |holder|
+ expect(holder).to have_selector('.discussion-next-btn')
+ end
+ end
+
+ it 'displays next discussion even if hidden' do
+ page.all('.note-discussion').each do |discussion|
+ page.within discussion do
+ click_link 'Toggle discussion'
+ end
+ end
+
+ page.within('.issuable-discussion #notes') do
+ expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ page.find('.discussion-next-btn').click
+ end
+
+ expect(find('.discussion-with-resolve-btn')).to have_selector('.btn', text: 'Resolve discussion')
+ end
+ end
+
+ context 'changes tab' do
+ it 'shows text with how many discussions' do
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to mark discussion as resolved' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ end
+
+ page.within '.diff-content .note' do
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+ click_button 'Unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'allows user to comment & resolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & resolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+
+ it 'allows user to comment & unresolve discussion' do
+ page.within '.diff-content' do
+ click_button 'Resolve discussion'
+
+ click_button 'Reply...'
+
+ find('.js-note-text').set 'testing'
+
+ click_button 'Comment & unresolve discussion'
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+ end
+ end
+
+ context 'as a guest' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.team << [guest, :guest]
+ login_as guest
+ end
+
+ context 'someone elses merge request' do
+ before do
+ visit_merge_request
+ end
+
+ it 'does not allow user to mark note as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.line-resolve-btn')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+
+ it 'does not allow user to mark discussion as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.btn', text: 'Resolve discussion')
+ end
+ end
+ end
+
+ context 'guest users merge request' do
+ before do
+ mr = create(:merge_request_with_diffs, source_project: project, source_branch: 'markdown', author: guest, title: "Bug")
+ create(:diff_note_on_merge_request, project: project, noteable: mr)
+ visit_merge_request(mr)
+ end
+
+ it 'allows user to mark a note as resolved' do
+ page.within '.diff-content .note' do
+ find('.line-resolve-btn').click
+
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ page.within '.diff-content' do
+ expect(page).to have_selector('.btn', text: 'Unresolve discussion')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('1/1 discussion resolved')
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ context 'no resolved comments' do
+ before do
+ visit_merge_request
+ end
+
+ it 'does not allow user to mark note as resolved' do
+ page.within '.diff-content .note' do
+ expect(page).not_to have_selector('.line-resolve-btn')
+ end
+
+ page.within '.line-resolve-all-container' do
+ expect(page).to have_content('0/1 discussion resolved')
+ end
+ end
+ end
+
+ context 'resolved comment' do
+ before do
+ note.resolve!(user)
+ visit_merge_request
+ end
+
+ it 'shows resolved icon' do
+ expect(page).to have_content '1/1 discussion resolved'
+
+ click_link 'Toggle discussion'
+ expect(page).to have_selector('.line-resolve-btn.is-active')
+ end
+
+ it 'does not allow user to click resolve button' do
+ expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ click_link 'Toggle discussion'
+
+ expect(page).to have_selector('.line-resolve-btn.is-disabled')
+ end
+ end
+ end
+
+ def visit_merge_request(mr = nil)
+ mr = mr || merge_request
+ visit namespace_project_merge_request_path(mr.project.namespace, mr.project, mr)
+ end
+end
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
new file mode 100644
index 00000000000..a818679a874
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -0,0 +1,207 @@
+require 'spec_helper'
+
+feature 'Diff notes', js: true, feature: true do
+ include WaitForAjax
+
+ before do
+ login_as :admin
+ @merge_request = create(:merge_request)
+ @project = @merge_request.source_project
+ end
+
+ context 'merge request diffs' do
+ let(:comment_button_class) { '.add-diff-note' }
+ let(:notes_holder_input_class) { 'js-temp-notes-holder' }
+ let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
+ let(:test_note_comment) { 'this is a test note!' }
+
+ context 'when hovering over a parallel view diff file' do
+ before(:each) do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
+ end
+
+ context 'with an old line on the left and no line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with no line on the left and a new line on the right' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an old line on the left and a new line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unchanged line on the left and an unchanged line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(line_holder, 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(line_holder, 'right')
+ end
+ end
+ end
+
+ context 'when hovering over an inline view diff file' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting line_holder
+ end
+ end
+
+ context 'when hovering over a diff discussion' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ end
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.line_holder', match: :first))
+ end
+ end
+ end
+
+ def should_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).to have_css comment_button_class
+
+ comment_on_line(line_holder, line)
+
+ assert_comment_persistence(line_holder)
+ end
+
+ def should_not_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).not_to have_css comment_button_class
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+
+ def comment_on_line(line_holder, line)
+ line[:num].find(comment_button_class).trigger 'click'
+ line_holder.find(:xpath, notes_holder_input_xpath)
+
+ notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_input[:class]).to include(notes_holder_input_class)
+
+ notes_holder_input.fill_in 'note[note]', with: test_note_comment
+ click_button 'Comment'
+ wait_for_ajax
+ end
+
+ def assert_comment_persistence(line_holder)
+ expect(line_holder).to have_xpath notes_holder_input_xpath
+
+ notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
+ expect(notes_holder_saved).to have_content test_note_comment
+ end
+ end
+end
diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb
new file mode 100644
index 00000000000..c9a0059645d
--- /dev/null
+++ b/spec/features/merge_requests/diffs_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+feature 'Diffs URL', js: true, feature: true do
+ before do
+ login_as :admin
+ @merge_request = create(:merge_request)
+ @project = @merge_request.source_project
+ end
+
+ context 'when visit with */* as accept header' do
+ before(:each) do
+ page.driver.add_header('Accept', '*/*')
+ end
+
+ it 'renders the notes' do
+ create :note_on_merge_request, project: @project, noteable: @merge_request, note: 'Rebasing with master'
+
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+
+ # Load notes and diff through AJAX
+ expect(page).to have_css('.note-text', visible: false, text: 'Rebasing with master')
+ expect(page).to have_css('.diffs.tab-pane.active')
+ end
+ end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 9e007ab7635..c77e719c5df 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -14,8 +14,19 @@ feature 'Edit Merge Request', feature: true do
end
context 'editing a MR' do
- it 'form should have class js-quick-submit' do
+ it 'has class js-quick-submit in form' do
expect(page).to have_selector('.js-quick-submit')
end
+
+ it 'warns about version conflict' do
+ merge_request.update(title: "New title")
+
+ fill_in 'merge_request_title', with: 'bug 345'
+ fill_in 'merge_request_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the merge request the same time you did'
+ end
end
end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index e3ecd60a5f3..bb0bb590a46 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -21,7 +21,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
end
context 'filters by upcoming milestone', js: true do
- it 'should not show issues with no expiry' do
+ it 'does not show issues with no expiry' do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -31,7 +31,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 0)
end
- it 'should show issues in future' do
+ it 'shows issues in future' do
milestone = create(:milestone, project: project, due_date: Date.tomorrow)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
@@ -42,7 +42,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 1)
end
- it 'should not show issues in past' do
+ it 'does not show issues in past' do
milestone = create(:milestone, project: project, due_date: Date.yesterday)
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
new file mode 100644
index 00000000000..577c910f11b
--- /dev/null
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Merge Request versions', js: true, feature: true do
+ before do
+ login_as :admin
+ merge_request = create(:merge_request, importing: true)
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project = merge_request.source_project
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: latest'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ describe 'switch between versions' do
+ before do
+ page.within '.mr-version-switch' do
+ find('.btn-link').click
+ click_link '6f6d7e7e'
+ end
+ end
+
+ it 'should show older version' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: 6f6d7e7e'
+ end
+
+ expect(page).to have_content '5 changed files'
+ end
+ end
+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
index 96f7b8c9932..60bc07bd1a0 100644
--- a/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -73,7 +73,7 @@ feature 'Merge When Build Succeeds', feature: true, js: true do
end
context 'Build is not active' do
- it "should not allow for enabling" do
+ it "does not allow for enabling" do
visit_merge_request(merge_request)
expect(page).not_to have_link "Merge When Build Succeeds"
end
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
new file mode 100644
index 00000000000..9c4c0525267
--- /dev/null
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+feature 'Pipelines for Merge Requests', feature: true, js: true do
+ include WaitForAjax
+
+ given(:user) { create(:user) }
+ given(:merge_request) { create(:merge_request) }
+ given(:project) { merge_request.target_project }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'with pipelines' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request pipelines tab' do
+ page.within('.merge-request-tabs') do
+ click_link('Pipelines')
+ end
+ wait_for_ajax
+
+ expect(page).to have_selector('.pipeline-actions')
+ end
+ end
+
+ context 'without pipelines' do
+ before do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ scenario 'user visits merge request page' do
+ page.within('.merge-request-tabs') do
+ expect(page).to have_no_link('Pipelines')
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index 1c130057c56..cabb8e455f9 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe 'Projects > Merge requests > User lists merge requests', feature: true do
+ include MergeRequestHelpers
include SortingHelper
let(:project) { create(:project, :public) }
@@ -23,10 +24,12 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
milestone: create(:milestone, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
+ # lfs in itself is not a great choice for the title if one wants to match the whole body content later on
+ # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name
create(:merge_request,
- title: 'lfs',
+ title: 'merge_lfs',
source_project: project,
- source_branch: 'lfs',
+ source_branch: 'merge_lfs',
created_at: 3.minutes.ago,
updated_at: 10.seconds.ago)
end
@@ -35,7 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
visit_merge_requests(project, assignee_id: IssuableFinder::NONE)
expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project))
- expect(page).to have_content 'lfs'
+ expect(page).to have_content 'merge_lfs'
expect(page).not_to have_content 'fix'
expect(page).not_to have_content 'markdown'
expect(count_merge_requests).to eq(1)
@@ -44,7 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
it 'filters on a specific assignee' do
visit_merge_requests(project, assignee_id: user.id)
- expect(page).not_to have_content 'lfs'
+ expect(page).not_to have_content 'merge_lfs'
expect(page).to have_content 'fix'
expect(page).to have_content 'markdown'
expect(count_merge_requests).to eq(2)
@@ -53,23 +56,23 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
it 'sorts by newest' do
visit_merge_requests(project, sort: sort_value_recently_created)
- expect(first_merge_request).to include('lfs')
- expect(last_merge_request).to include('fix')
+ expect(first_merge_request).to include('fix')
+ expect(last_merge_request).to include('merge_lfs')
expect(count_merge_requests).to eq(3)
end
it 'sorts by oldest' do
visit_merge_requests(project, sort: sort_value_oldest_created)
- expect(first_merge_request).to include('fix')
- expect(last_merge_request).to include('lfs')
+ expect(first_merge_request).to include('merge_lfs')
+ expect(last_merge_request).to include('fix')
expect(count_merge_requests).to eq(3)
end
it 'sorts by last updated' do
visit_merge_requests(project, sort: sort_value_recently_updated)
- expect(first_merge_request).to include('lfs')
+ expect(first_merge_request).to include('merge_lfs')
expect(count_merge_requests).to eq(3)
end
@@ -143,18 +146,6 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true
end
end
- def visit_merge_requests(project, opts = {})
- visit namespace_project_merge_requests_path(project.namespace, project, opts)
- end
-
- def first_merge_request
- page.all('ul.mr-list > li').first.text
- end
-
- def last_merge_request
- page.all('ul.mr-list > li').last.text
- end
-
def count_merge_requests
page.all('ul.mr-list > li').count
end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
new file mode 100644
index 00000000000..d9ef0d18074
--- /dev/null
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -0,0 +1,32 @@
+require 'rails_helper'
+
+feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+
+ it_behaves_like 'issuable record that supports slash commands in its description and notes', :merge_request do
+ let(:issuable) { create(:merge_request, source_project: project) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+ end
+
+ describe 'adding a due date from note' do
+ before do
+ project.team << [user, :master]
+ login_with(user)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not recognize the command nor create a note' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/due 2016-08-28"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ end
+ end
+end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index c2c7acff3e8..c43661e5681 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -13,7 +13,7 @@ feature 'Milestone', feature: true do
end
feature 'Create a milestone' do
- scenario 'should show an informative message for a new issue' do
+ scenario 'shows an informative message for a new issue' do
visit new_namespace_project_milestone_path(project.namespace, project)
page.within '.milestone-form' do
fill_in "milestone_title", with: '8.7'
@@ -25,7 +25,7 @@ feature 'Milestone', feature: true do
end
feature 'Open a milestone with closed issues' do
- scenario 'should show an informative message' do
+ scenario 'shows an informative message' do
create(:issue, title: "Bugfix1", project: project, milestone: milestone, state: "closed")
visit namespace_project_milestone_path(project.namespace, project, milestone)
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 5174168713c..f1c522155d3 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -23,7 +23,7 @@ describe 'Comments', feature: true do
subject { page }
describe 'the note form' do
- it 'should be valid' do
+ it 'is valid' do
is_expected.to have_css('.js-main-target-form', visible: true, count: 1)
expect(find('.js-main-target-form input[type=submit]').value).
to eq('Comment')
@@ -39,7 +39,7 @@ describe 'Comments', feature: true do
end
end
- it 'should have enable submit button and preview button' do
+ it 'has enable submit button and preview button' do
page.within('.js-main-target-form') do
expect(page).not_to have_css('.js-comment-button[disabled]')
expect(page).to have_css('.js-md-preview-button', visible: true)
@@ -57,7 +57,7 @@ describe 'Comments', feature: true do
end
end
- it 'should be added and form reset' do
+ it 'is added and form reset' do
is_expected.to have_content('This is awsome!')
page.within('.js-main-target-form') do
expect(page).to have_no_field('note[note]', with: 'This is awesome!')
@@ -70,7 +70,7 @@ describe 'Comments', feature: true do
end
describe 'when editing a note', js: true do
- it 'should contain the hidden edit form' do
+ it 'contains the hidden edit form' do
page.within("#note_#{note.id}") do
is_expected.to have_css('.note-edit-form', visible: false)
end
@@ -82,7 +82,7 @@ describe 'Comments', feature: true do
find(".js-note-edit").click
end
- it 'should show the note edit form and hide the note body' do
+ it 'shows the note edit form and hide the note body' do
page.within("#note_#{note.id}") do
expect(find('.current-note-edit-form', visible: true)).to be_visible
expect(find('.note-edit-form', visible: true)).to be_visible
@@ -135,6 +135,28 @@ describe 'Comments', feature: true do
end
end
+ describe 'Handles cross-project system notes', js: true, feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ 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)}") }
+
+ it 'shows the system note' do
+ login_as :admin
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_css('.system-note')
+ end
+
+ it 'hides redacted system note' do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).not_to have_css('.system-note')
+ end
+ end
+
describe 'On a merge request diff', js: true, feature: true do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.source_project }
@@ -212,7 +234,7 @@ describe 'Comments', feature: true do
end
end
- it 'should be added as discussion' do
+ it 'adds as discussion' do
is_expected.to have_content('Another comment on line 10')
is_expected.to have_css('.notes_holder')
is_expected.to have_css('.notes_holder .note', count: 1)
@@ -231,6 +253,7 @@ describe 'Comments', feature: true do
end
def click_diff_line(data = line_code)
- execute_script("$('button[data-line-code=\"#{data}\"]').click()")
+ find(".line_holder[id='#{data}'] td.line_content").hover
+ find(".line_holder[id='#{data}'] button").trigger('click')
end
end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index c7c00a3266a..a78a1c9c890 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -12,17 +12,17 @@ feature 'Member autocomplete', feature: true do
end
shared_examples "open suggestions" do
- it 'suggestions are displayed' do
+ it 'displays suggestions' do
expect(page).to have_selector('.atwho-view', visible: true)
end
- it 'author is suggested' do
+ it 'suggests author' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
end
end
- it 'participant is suggested' do
+ it 'suggests participant' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username)
end
diff --git a/spec/features/pipelines_settings_spec.rb b/spec/features/pipelines_settings_spec.rb
new file mode 100644
index 00000000000..dcc364a3d01
--- /dev/null
+++ b/spec/features/pipelines_settings_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+feature "Pipelines settings", feature: true do
+ include GitlabRoutingHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_pipelines_settings_path(project.namespace, project)
+ end
+
+ context 'for developer' do
+ given(:role) { :developer }
+
+ scenario 'to be disallowed to view' do
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ context 'for master' do
+ given(:role) { :master }
+
+ scenario 'be allowed to change' do
+ fill_in('Test coverage parsing', with: 'coverage_regex')
+ click_on 'Save changes'
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
+ end
+ end
+end
diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb
deleted file mode 100644
index e7ee0aaea3c..00000000000
--- a/spec/features/pipelines_spec.rb
+++ /dev/null
@@ -1,189 +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_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!(:running) { create(:ci_build, :running, pipeline: pipeline, stage: 'test', commands: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- 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!(:failed) { create(:ci_build, :failed, pipeline: pipeline, stage: 'test', commands: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- 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-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 { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it 'not be cancelable' do
- expect(page).not_to have_link('Cancel')
- end
-
- it 'pipeline is running' do
- expect(page).to have_selector('.ci-running')
- end
- end
-
- context 'when failed' do
- let!(:running) { create(:generic_commit_status, status: 'failed', pipeline: pipeline, stage: 'test') }
-
- before { visit namespace_project_pipelines_path(project.namespace, project) }
-
- it 'not be retryable' do
- expect(page).not_to have_link('Retry')
- end
-
- it 'pipeline is failed' 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 'without artifacts' do
- let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') }
-
- 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')
- @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 'showing 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')
- 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_content('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
- 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('Create for', 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('Create for', with: 'invalid reference')
- click_on 'Create pipeline'
- end
-
- it { expect(page).to have_content('Reference not found') }
- end
- end
-end
diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb
index c80253fead8..c3d8c349ca4 100644
--- a/spec/features/profile_spec.rb
+++ b/spec/features/profile_spec.rb
@@ -15,7 +15,7 @@ describe 'Profile account page', feature: true do
it { expect(page).to have_content('Remove account') }
- it 'should delete the account' do
+ it 'deletes the account' do
expect { click_link 'Delete account' }.to change { User.count }.by(-1)
expect(current_path).to eq(new_user_session_path)
end
@@ -27,7 +27,7 @@ describe 'Profile account page', feature: true do
visit profile_account_path
end
- it 'should not have option to remove account' do
+ it 'does not have option to remove account' do
expect(page).not_to have_content('Remove account')
expect(current_path).to eq(profile_account_path)
end
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
new file mode 100644
index 00000000000..4cbdd89d46f
--- /dev/null
+++ b/spec/features/profiles/password_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe 'Profile > Password', feature: true do
+ let(:user) { create(:user, password_automatically_set: true) }
+
+ before do
+ login_as(user)
+ visit edit_profile_password_path
+ end
+
+ def fill_passwords(password, confirmation)
+ fill_in 'New password', with: password
+ fill_in 'Password confirmation', with: confirmation
+
+ click_button 'Save password'
+ end
+
+ context 'User with password automatically set' do
+ describe 'User puts different passwords in the field and in the confirmation' do
+ it 'shows an error message' do
+ fill_passwords('mypassword', 'mypassword2')
+
+ page.within('.alert-danger') do
+ expect(page).to have_content("Password confirmation doesn't match Password")
+ end
+ end
+
+ it 'does not contains the current password field after an error' do
+ fill_passwords('mypassword', 'mypassword2')
+
+ expect(page).to have_no_field('user[current_password]')
+ end
+ end
+
+ describe 'User puts the same passwords in the field and in the confirmation' do
+ it 'shows a success message' do
+ fill_passwords('mypassword', 'mypassword')
+
+ page.within('.flash-notice') do
+ expect(page).to have_content('Password was successfully updated. Please login with it')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 787bf42d048..d14a1158b67 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -68,10 +68,14 @@ describe 'Profile > Preferences', feature: true do
allowing_for_delay do
find('#logo').click
+
+ expect(page).to have_content("You don't have starred projects yet")
expect(page.current_path).to eq starred_dashboard_projects_path
end
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
end
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
new file mode 100644
index 00000000000..5972e7f31c2
--- /dev/null
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+feature 'test coverage badge' do
+ given!(:user) { create(:user) }
+ given!(:project) { create(:project, :private) }
+
+ context 'when user has access to view badge' do
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ end
+
+ scenario 'user requests coverage badge image for pipeline' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 100, name: 'test:1')
+ create_build(pipeline, coverage: 90, name: 'test:2')
+ end
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('95%')
+ end
+
+ scenario 'user requests coverage badge for specific job' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 50, name: 'test:1')
+ create_build(pipeline, coverage: 50, name: 'test:2')
+ create_build(pipeline, coverage: 85, name: 'coverage')
+ end
+
+ show_test_coverage_badge(job: 'coverage')
+
+ expect_coverage_badge('85%')
+ end
+
+ scenario 'user requests coverage badge for pipeline without coverage' do
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: nil, name: 'test')
+ end
+
+ show_test_coverage_badge
+
+ expect_coverage_badge('unknown')
+ end
+ end
+
+ context 'when user does not have access to view badge' do
+ background { login_as(user) }
+
+ scenario 'user requests test coverage badge image' do
+ show_test_coverage_badge
+
+ expect(page).to have_http_status(404)
+ end
+ end
+
+ def create_pipeline
+ opts = { project: project, ref: 'master', sha: project.commit.id }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+
+ def create_build(pipeline, coverage:, name:)
+ opts = { pipeline: pipeline, coverage: coverage, name: name }
+
+ create(:ci_build, :success, opts)
+ end
+
+ def show_test_coverage_badge(job: nil)
+ visit coverage_namespace_project_badges_path(
+ project.namespace, project, ref: :master, job: job, format: :svg)
+ end
+
+ def expect_coverage_badge(coverage)
+ svg = Nokogiri::XML.parse(page.body)
+ expect(page.response_headers['Content-Type']).to include('image/svg+xml')
+ expect(svg.at(%Q{text:contains("#{coverage}")})).to be_truthy
+ end
+end
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 01e90618a98..67a4a5d1ab1 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -6,28 +6,46 @@ feature 'list of badges' do
project = create(:project)
project.team << [user, :master]
login_as(user)
- visit namespace_project_badges_path(project.namespace, project)
+ visit namespace_project_pipelines_settings_path(project.namespace, project)
end
- scenario 'user displays list of badges' do
- expect(page).to have_content 'build status'
- expect(page).to have_content 'Markdown'
- expect(page).to have_content 'HTML'
- expect(page).to have_css('.highlight', count: 2)
- expect(page).to have_xpath("//img[@alt='build status']")
+ scenario 'user wants to see build status badge' do
+ page.within('.build-status') do
+ expect(page).to have_content 'build status'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='build status']")
- page.within('.highlight', match: :first) do
- expect(page).to have_content 'badges/master/build.svg'
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/build.svg'
+ end
end
end
- scenario 'user changes current ref on badges list page', js: true do
- first('.js-project-refs-dropdown').click
+ scenario 'user wants to see coverage report badge' do
+ page.within('.coverage-report') do
+ expect(page).to have_content 'coverage report'
+ expect(page).to have_content 'Markdown'
+ expect(page).to have_content 'HTML'
+ expect(page).to have_css('.highlight', count: 2)
+ expect(page).to have_xpath("//img[@alt='coverage report']")
- page.within '.project-refs-form' do
- click_link 'improve/awesome'
+ page.within('.highlight', match: :first) do
+ expect(page).to have_content 'badges/master/coverage.svg'
+ end
end
+ end
+
+ scenario 'user changes current ref of build status badge', js: true do
+ page.within('.build-status') do
+ first('.js-project-refs-dropdown').click
- expect(page).to have_content 'badges/improve/awesome/build.svg'
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
+
+ expect(page).to have_content 'badges/improve/awesome/build.svg'
+ end
end
end
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
new file mode 100644
index 00000000000..63878c55421
--- /dev/null
+++ b/spec/features/projects/branches/delete_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Delete branch', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'destroys tooltip' do
+ first('.remove-row').hover
+ expect(page).to have_selector('.tooltip')
+
+ first('.remove-row').click
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.tooltip')
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
new file mode 100644
index 00000000000..1b14945bf0a
--- /dev/null
+++ b/spec/features/projects/branches_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'Branches', feature: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ before do
+ login_as :user
+ project.team << [@user, :developer]
+ end
+
+ describe 'Initial branches page' do
+ it 'shows all the branches' do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
+ expect(page).to have_content("Protected branches can be managed in project settings")
+ end
+ end
+
+ describe 'Find branches' do
+ it 'shows filtered branches', js: true do
+ visit namespace_project_branches_path(project.namespace, project)
+
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ end
+ end
+end
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 1b4ff6b6f1b..e45e3a36d01 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include WaitForAjax
describe 'Cherry-pick Commits' do
let(:project) { create(:project) }
@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
before do
login_as :user
project.team << [@user, :master]
- visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
end
context "I cherry-pick a commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a merge commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit that was previously cherry-picked" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit in a new merge request" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
end
end
+
+ context "I cherry-pick a commit from a different branch", js: true do
+ it do
+ find('.commit-action-buttons a.dropdown-toggle').click
+ find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'master'
+ end
+
+ wait_for_ajax
+
+ page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+ click_link 'feature'
+ end
+
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
end
diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb
new file mode 100644
index 00000000000..fe047e00409
--- /dev/null
+++ b/spec/features/projects/files/editing_a_file_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+feature 'User wants to edit a file', feature: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:commit_params) do
+ {
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ commit_message: "Committing First Update",
+ file_path: ".gitignore",
+ file_content: "First Update",
+ last_commit_sha: Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch,
+ ".gitignore").sha
+ }
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_edit_blob_path(project.namespace, project,
+ File.join(project.default_branch, '.gitignore'))
+ end
+
+ scenario 'file has been updated since the user opened the edit page' do
+ Files::UpdateService.new(project, user, commit_params).execute
+
+ click_button 'Commit Changes'
+
+ expect(page).to have_content 'Someone edited the file the same time you did.'
+ end
+end
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
new file mode 100644
index 00000000000..10b91d8990b
--- /dev/null
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'User views files page', feature: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:forked_project_with_submodules) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref)
+ end
+
+ scenario 'user sees folders and submodules sorted together, followed by files' do
+ rows = all('td.tree-item-file-name').map(&:text)
+ tree = project.repository.tree
+
+ folders = tree.trees.map(&:name)
+ files = tree.blobs.map(&:name)
+ submodules = tree.submodules.map do |submodule|
+ submodule.name + " @ " + submodule.id[0..7]
+ end
+
+ sorted_titles = (folders + submodules).sort + files
+
+ expect(rows).to eq(sorted_titles)
+ end
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index e1e105e6bbe..a521ce50f35 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -23,7 +23,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
@@ -39,6 +39,7 @@ feature 'project owner creates a license file', feature: true, js: true do
scenario 'project master creates a license file from the "Add license" link' do
click_link 'Add License'
+ expect(page).to have_content('New File')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
expect(find('#file_name').value).to eq('LICENSE')
@@ -46,7 +47,7 @@ feature 'project owner creates a license file', feature: true, js: true do
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 67aac25e427..4453b6d485f 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -14,6 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f
visit namespace_project_path(project.namespace, project)
click_link 'Create empty bare repository'
click_on 'LICENSE'
+ expect(page).to have_content('New File')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
@@ -22,7 +23,7 @@ feature 'project owner sees a link to create a license file in empty project', f
select_template('MIT License')
- file_content = find('.file-content')
+ file_content = first('.file-editor')
expect(file_content).to have_content('The MIT License (MIT)')
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 00000000000..1a71a03fbd9
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+ include Select2Helper
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ context 'setting an expiration date for a group link' do
+ before do
+ visit namespace_project_group_links_path(project.namespace, project)
+
+ select2 group.id, from: '#link_group_id'
+ fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ click_on 'Share'
+ end
+
+ it 'shows the expiration time with a warning class' do
+ page.within('.enabled-groups') do
+ expect(page).to have_content('expires in 4 days')
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index bc3bf53fe9d..f707ccf4e93 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -3,59 +3,97 @@ require 'spec_helper'
feature 'project import', feature: true, js: true do
include Select2Helper
- let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: "asd", owner: user) }
+ let(:admin) { create(:admin) }
+ let(:normal_user) { create(:user) }
+ let!(:namespace) { create(:namespace, name: "asd", owner: admin) }
let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') }
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:project) { Project.last }
+ let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
- login_as(user)
end
after(:each) do
FileUtils.rm_rf(export_path, secure: true)
end
- scenario 'user imports an exported project successfully' do
- expect(Project.all.count).to be_zero
+ context 'admin user' do
+ before do
+ login_as(admin)
+ end
+
+ scenario 'user imports an exported project successfully' do
+ expect(Project.all.count).to be_zero
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: 'test-project-path', visible: true
+ click_link 'GitLab export'
+
+ expect(page).to have_content('GitLab project export')
+ expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+
+ attach_file('file', file)
+
+ click_on 'Import project' # import starts
+
+ expect(project).not_to be_nil
+ expect(project.issues).not_to be_empty
+ expect(project.merge_requests).not_to be_empty
+ expect(project_hook).to exist
+ expect(wiki_exists?).to be true
+ expect(project.import_status).to eq('finished')
+ end
+
+ scenario 'invalid project' do
+ project = create(:project, namespace_id: 2)
+
+ visit new_project_path
+
+ select2('2', from: '#project_namespace_id')
+ fill_in :project_path, with: project.name, visible: true
+ click_link 'GitLab export'
- visit new_project_path
+ attach_file('file', file)
+ click_on 'Import project'
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: 'test-project-path', visible: true
- click_link 'GitLab export'
+ page.within('.flash-container') do
+ expect(page).to have_content('Project could not be imported')
+ end
+ end
+
+ scenario 'project with no name' do
+ create(:project, namespace_id: 2)
- expect(page).to have_content('GitLab project export')
- expect(URI.parse(current_url).query).to eq('namespace_id=2&path=test-project-path')
+ visit new_project_path
- attach_file('file', file)
+ select2('2', from: '#project_namespace_id')
- click_on 'Import project' # import starts
+ # click on disabled element
+ find(:link, 'GitLab export').trigger('click')
- expect(project).not_to be_nil
- expect(project.issues).not_to be_empty
- expect(project.merge_requests).not_to be_empty
- expect(project.repo_exists?).to be true
- expect(wiki_exists?).to be true
- expect(project.import_status).to eq('finished')
+ page.within('.flash-container') do
+ expect(page).to have_content('Please enter path and name')
+ end
+ end
end
- scenario 'invalid project' do
- project = create(:project, namespace_id: 2)
+ context 'normal user' do
+ before do
+ login_as(normal_user)
+ end
- visit new_project_path
+ scenario 'non-admin user is not allowed to import a project' do
+ expect(Project.all.count).to be_zero
- select2('2', from: '#project_namespace_id')
- fill_in :project_path, with: project.name, visible: true
- click_link 'GitLab export'
+ visit new_project_path
- attach_file('file', file)
- click_on 'Import project'
+ fill_in :project_path, with: 'test-project-path', visible: true
- page.within('.flash-container') do
- expect(page).to have_content('Project could not be imported')
+ expect(page).not_to have_content('GitLab export')
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
new file mode 100644
index 00000000000..4a83740621a
--- /dev/null
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+feature 'issuable templates', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'user creates an issue using templates' do
+ let(:template_content) { 'this is a test "bug" template' }
+ let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ visit edit_namespace_project_issue_path project.namespace, project, issue
+ fill_in :'issue[title]', with: 'test issue title'
+ end
+
+ scenario 'user selects "bug" template' do
+ select_template 'bug'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path project.namespace, project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ context 'user creates a merge request from a forked project using templates' do
+ let(:template_content) { 'this is a test "feature-proposal" template' }
+ let(:fork_user) { create(:user) }
+ let(:fork_project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project) }
+
+ background do
+ logout
+ project.team << [fork_user, :developer]
+ fork_project.team << [fork_user, :master]
+ create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project)
+ login_as fork_user
+ fork_project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false)
+ visit edit_namespace_project_merge_request_path fork_project.namespace, fork_project, merge_request
+ fill_in :'merge_request[title]', with: 'test merge request title'
+ end
+
+ scenario 'user selects "feature-proposal" template' do
+ select_template 'feature-proposal'
+ wait_for_ajax
+ preview_template
+ save_changes
+ end
+ end
+
+ def preview_template
+ click_link 'Preview'
+ expect(page).to have_content template_content
+ end
+
+ def save_changes
+ click_button "Save changes"
+ expect(page).to have_content template_content
+ end
+
+ def select_template(name)
+ first('.js-issuable-selector').click
+ first('.js-issuable-selector-wrap .dropdown-content a', text: name).click
+ end
+end
diff --git a/spec/features/projects/issues/list_spec.rb b/spec/features/projects/issues/list_spec.rb
new file mode 100644
index 00000000000..3137af074ca
--- /dev/null
+++ b/spec/features/projects/issues/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Issues List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index 6a39c302f55..cb7495da8eb 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -76,7 +76,7 @@ feature 'Prioritize labels', feature: true do
expect(page.all('li').last).to have_content('bug')
end
- visit current_url
+ refresh
wait_for_ajax
page.within('.prioritized-labels') do
@@ -87,7 +87,7 @@ feature 'Prioritize labels', feature: true do
end
context 'as a guest' do
- it 'can not prioritize labels' do
+ it 'does not prioritize labels' do
user = create(:user)
guest = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
@@ -102,7 +102,7 @@ feature 'Prioritize labels', feature: true do
end
context 'as a non signed in user' do
- it 'can not prioritize labels' do
+ it 'does not prioritize labels' do
user = create(:user)
project = create(:project, name: 'test', namespace: user.namespace)
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..430c384ac2e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:new_member) { create(:user) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: '2016-08-10'
+ click_on 'Add users to project'
+ end
+
+ page.within '.project_member:first-child' do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ scenario 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.project_member:first-child' do
+ click_on 'Edit'
+ fill_in 'Access expiration date', with: '2016-08-09'
+ click_on 'Save'
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ 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 f2fe3ef364d..56ede8eb5be 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -11,6 +11,13 @@ feature 'Projects > Members > User requests access', feature: true do
visit namespace_project_path(project.namespace, project)
end
+ scenario 'request access feature is disabled' do
+ project.update_attributes(request_access_enabled: false)
+ visit namespace_project_path(project.namespace, project)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
scenario 'user can request access to a project' do
perform_enqueued_jobs { click_link 'Request Access' }
diff --git a/spec/features/projects/merge_requests/list_spec.rb b/spec/features/projects/merge_requests/list_spec.rb
new file mode 100644
index 00000000000..5dd58ad66a7
--- /dev/null
+++ b/spec/features/projects/merge_requests/list_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+feature 'Merge Requests List' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ scenario 'user does not see create new list button' do
+ create(:merge_request, source_project: project)
+
+ visit namespace_project_merge_requests_path(project.namespace, project)
+
+ expect(page).not_to have_selector('.js-new-board-list')
+ end
+end
diff --git a/spec/features/projects/pipelines_spec.rb b/spec/features/projects/pipelines_spec.rb
new file mode 100644
index 00000000000..47482bc3cc9
--- /dev/null
+++ b/spec/features/projects/pipelines_spec.rb
@@ -0,0 +1,262 @@
+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_content('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
new file mode 100644
index 00000000000..3de25d7af7d
--- /dev/null
+++ b/spec/features/projects/project_settings_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe 'Edit Project Settings', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') }
+
+ before do
+ login_as(user)
+ project.team << [user, :master]
+ end
+
+ describe 'Project settings', js: true do
+ it 'shows errors for invalid project name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'project_name_edit', with: 'foo&bar'
+
+ 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_button 'Save changes'
+ end
+ end
+
+ describe 'Rename repository' do
+ it 'shows errors for invalid project path/name' do
+ visit edit_namespace_project_path(project.namespace, project)
+
+ fill_in 'Project name', with: 'foo&bar'
+ fill_in 'Path', with: 'foo&bar'
+
+ click_button 'Rename project'
+
+ 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 "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
+ end
+ end
+end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
new file mode 100644
index 00000000000..b3ba40b35af
--- /dev/null
+++ b/spec/features/projects/ref_switcher_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+feature 'Ref switcher', feature: true, js: true do
+ include WaitForAjax
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ visit namespace_project_tree_path(project.namespace, project, 'master')
+ end
+
+ it 'allow user to change ref by enter key' do
+ click_button 'master'
+ wait_for_ajax
+
+ page.within '.project-refs-form' do
+ input = find('input[type="search"]')
+ input.set 'expand'
+
+ input.native.send_keys :down
+ input.native.send_keys :down
+ input.native.send_keys :enter
+
+ expect(page).to have_content 'expand-collapse-files'
+ 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
new file mode 100644
index 00000000000..16541f51d98
--- /dev/null
+++ b/spec/features/projects/slack_service/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) { 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/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
new file mode 100644
index 00000000000..a1c386ddc18
--- /dev/null
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+feature 'Projects > Wiki > User previews markdown changes', feature: true, js: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:wiki_content) do
+ <<-HEREDOC
+[regular link](regular)
+[relative link 1](../relative)
+[relative link 2](./relative)
+[relative link 3](./e/f/relative)
+ HEREDOC
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_path(project.namespace, project)
+ click_link 'Wiki'
+ WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
+ end
+
+ context "while creating a new wiki page" do
+ context "when there are no spaces or hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a/b/c/d'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are spaces in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a page/b page/c page/d page'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+ end
+
+ context "while editing a wiki page" do
+ def create_wiki_page(path)
+ click_link 'New Page'
+ fill_in :new_wiki_path, with: path
+ click_button 'Create Page'
+ fill_in :wiki_content, with: 'content'
+ click_on "Create page"
+ end
+
+ context "when there are no spaces or hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a/b/c/d'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are spaces in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a page/b page/c page/d page'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+
+ context "when there are hyphens in the page name" do
+ it "rewrites relative links as expected" do
+ create_wiki_page 'a-page/b-page/c-page/d-page'
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: wiki_content
+ click_on "Preview"
+
+ expect(page).to have_content("regular link")
+
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
+ expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ end
+ end
+ 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 7e6eef65873..7afd83b7250 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -30,18 +30,48 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute
end
- scenario 'via the "new wiki page" page', js: true do
- click_link 'New Page'
+ context 'via the "new wiki page" page' do
+ scenario 'when the wiki page has a single word name', js: true do
+ click_link 'New Page'
- fill_in :new_wiki_path, with: 'foo'
- click_button 'Create Page'
+ fill_in :new_wiki_path, with: 'foo'
+ click_button 'Create Page'
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Create page'
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ 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('My awesome wiki!')
+ expect(page).to have_content('Foo')
+ expect(page).to have_content("last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'when the wiki page has spaces in the name', js: true do
+ click_link 'New Page'
+
+ fill_in :new_wiki_path, with: 'Spaces in the name'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ 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('My awesome wiki!')
+ end
+
+ scenario 'when the wiki page has hyphens in the name', js: true do
+ click_link 'New Page'
+
+ fill_in :new_wiki_path, with: 'hyphens-in-the-name'
+ click_button 'Create Page'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ 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('My awesome wiki!')
+ end
end
end
end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 6fa8298d489..e00d85904d5 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -44,7 +44,7 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'should remove fork' do
+ it 'removes fork' do
expect(page).to have_content 'Remove fork relationship'
remove_with_confirm('Remove fork relationship', project.path)
@@ -65,7 +65,7 @@ feature 'Project', feature: true do
visit edit_namespace_project_path(project.namespace, project)
end
- it 'should remove project' do
+ it 'removes project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
end
end
@@ -82,7 +82,7 @@ feature 'Project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- it 'click toggle and show dropdown', js: true do
+ it 'clicks toggle and shows dropdown', js: true do
find('.js-projects-dropdown-toggle').click
expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 1)
end
@@ -102,7 +102,7 @@ feature 'Project', feature: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
- it 'click toggle and show dropdown' do
+ it 'clicks toggle and shows dropdown' do
find('.js-projects-dropdown-toggle').click
expect(page).to have_css('.dropdown-menu-projects .dropdown-content li', count: 2)
@@ -115,6 +115,35 @@ feature 'Project', feature: true do
end
end
+ describe 'tree view (default view is set to Files)' do
+ let(:user) { create(:user, project_view: 'files') }
+ let(:project) { create(:forked_project_with_submodules) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'has working links to files' do
+ click_link('PROCESS.md')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to directories' do
+ click_link('encoding')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to submodules' do
+ click_link('645f6c4c')
+
+ expect(page.status_code).to eq(200)
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
new file mode 100644
index 00000000000..395c61a4743
--- /dev/null
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -0,0 +1,71 @@
+RSpec.shared_examples "protected branches > access control > CE" do
+ ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can push to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_push_button = find(".js-allowed-to-push")
+
+ unless allowed_to_push_button.text == access_type_name
+ allowed_to_push_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can push to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-push").click
+ within('.js-allowed-to-push-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+
+ ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can merge to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_merge_button = find(".js-allowed-to-merge")
+
+ unless allowed_to_merge_button.text == access_type_name
+ allowed_to_merge_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can merge to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-merge").click
+ within('.js-allowed-to-merge-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index d94dee0c797..1a3f7b970f6 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
+Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
+ include WaitForAjax
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project) }
@@ -9,7 +12,7 @@ feature 'Projected Branches', feature: true, js: true do
def set_protected_branch_name(branch_name)
find(".js-protected-branch-select").click
find(".dropdown-input-field").set(branch_name)
- click_on "Create Protected Branch: #{branch_name}"
+ click_on("Create wildcard #{branch_name}")
end
describe "explicit protected branches" do
@@ -69,7 +72,10 @@ feature 'Projected Branches', feature: true, js: true do
project.repository.add_branch(user, 'production-stable', 'master')
project.repository.add_branch(user, 'staging-stable', 'master')
project.repository.add_branch(user, 'development', 'master')
- create(:protected_branch, project: project, name: "*-stable")
+
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('*-stable')
+ click_on "Protect"
visit namespace_project_protected_branches_path(project.namespace, project)
click_on "2 matching branches"
@@ -81,4 +87,8 @@ feature 'Projected Branches', feature: true, js: true do
end
end
end
+
+ describe "access control" do
+ include_examples "protected branches > access control > CE"
+ end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index d0a301038c4..dcd3a2f17b0 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -12,7 +12,7 @@ describe "Search", feature: true do
visit search_path
end
- it 'top right search form is not present' do
+ it 'does not show top right search form' do
expect(page).not_to have_selector('.search')
end
@@ -28,6 +28,26 @@ describe "Search", feature: true do
end
context 'search for comments' do
+ context 'when comment belongs to a invalid commit' do
+ let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') }
+
+ before { note.update_attributes(commit_id: 12345678) }
+
+ it 'finds comment' do
+ visit namespace_project_path(project.namespace, project)
+
+ page.within '.search' do
+ fill_in 'search', with: note.note
+ click_button 'Go'
+ end
+
+ click_link 'Comments'
+
+ expect(page).to have_text("Commit deleted")
+ expect(page).to have_text("12345678")
+ end
+ end
+
it 'finds a snippet' do
snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
note = create(:note,
@@ -51,21 +71,31 @@ describe "Search", feature: true do
end
describe 'Right header search field', feature: true do
+ it 'allows enter key to search', js: true do
+ visit namespace_project_path(project.namespace, project)
+ fill_in 'search', with: 'gitlab'
+ find('#search').native.send_keys(:enter)
+
+ page.within '.title' do
+ expect(page).to have_content 'Search'
+ end
+ end
+
describe 'Search in project page' do
before do
visit namespace_project_path(project.namespace, project)
end
- it 'top right search form is present' do
+ it 'shows top right search form' do
expect(page).to have_selector('#search')
end
- it 'top right search form contains location badge' do
+ it 'contains location badge in top right search form' do
expect(page).to have_selector('.has-location-badge')
end
context 'clicking the search field', js: true do
- it 'should show category search dropdown' do
+ it 'shows category search dropdown' do
page.find('#search').click
expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
@@ -77,7 +107,7 @@ describe "Search", feature: true do
page.find('#search').click
end
- it 'should take user to her issues page when issues assigned is clicked' do
+ it 'takes user to her issues page when issues assigned is clicked' do
find('.dropdown-menu').click_link 'Issues assigned to me'
sleep 2
@@ -85,7 +115,7 @@ describe "Search", feature: true do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her issues page when issues authored is clicked' do
+ it 'takes user to her issues page when issues authored is clicked' do
find('.dropdown-menu').click_link "Issues I've created"
sleep 2
@@ -93,7 +123,7 @@ describe "Search", feature: true do
expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her MR page when MR assigned is clicked' do
+ it 'takes user to her MR page when MR assigned is clicked' do
find('.dropdown-menu').click_link 'Merge requests assigned to me'
sleep 2
@@ -101,7 +131,7 @@ describe "Search", feature: true do
expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name)
end
- it 'should take user to her MR page when MR authored is clicked' do
+ it 'takes user to her MR page when MR authored is clicked' do
find('.dropdown-menu').click_link "Merge requests I've created"
sleep 2
@@ -117,7 +147,7 @@ describe "Search", feature: true do
end
end
- it 'should not display the category search dropdown' do
+ it 'does not display the category search dropdown' do
expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
end
end
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb..40f773956d1 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
+ describe "GET /koding" do
+ subject { koding_path }
+
+ context 'with Koding enabled' do
+ before do
+ stub_application_setting(koding_enabled?: true)
+ end
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ end
+ end
+
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 13d980a326f..b6acc509342 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -426,4 +426,23 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ 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 }
+ end
end
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ac9690cc127..ccb5c06dab0 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -362,4 +362,23 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ 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 }
+ end
end
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 737897de52b..985663e7c98 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -426,4 +426,23 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for :external }
it { is_expected.to be_denied_for :visitor }
end
+
+ describe "GET /:project_path/container_registry" do
+ before do
+ stub_container_registry_tags('latest')
+ stub_container_registry_config(enabled: true)
+ end
+
+ 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 }
+ end
end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 00000000000..e74a51acede
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ project.team << [user, :developer]
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0bdb1628c74..32544f3f538 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -24,7 +24,7 @@ describe 'Dashboard Todos', feature: true do
visit dashboard_todos_path
end
- it 'todo is present' do
+ it 'has todo present' do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
@@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content("You're all done!")
end
end
+
+ context 'todo is stale on the page' do
+ before do
+ todos = TodosFinder.new(user, state: :pending).execute
+ TodoService.new.mark_todos_as_done(todos, user)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
end
context 'User has Todos with labels spanning multiple projects' do
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 14613754f74..a46e48c76ed 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,11 +1,23 @@
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+ include WaitForAjax
+
+ before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
+
+ def manage_two_factor_authentication
+ click_on 'Manage Two-Factor Authentication'
+ expect(page).to have_content("Setup New U2F Device")
+ wait_for_ajax
+ end
+
def register_u2f_device(u2f_device = nil)
- u2f_device ||= FakeU2fDevice.new(page)
+ name = FFaker::Name.first_name
+ u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device'
expect(page).to have_content('Your device was successfully set up')
+ fill_in "Pick a name", with: name
click_on 'Register U2F Device'
u2f_device
end
@@ -30,13 +42,14 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
end
describe 'when 2FA via OTP is enabled' do
- it 'allows registering a new device' do
+ it 'allows registering a new device with a name' do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
expect(page.body).to match("You've already enabled two-factor authentication using mobile")
- register_u2f_device
+ u2f_device = register_u2f_device
+ expect(page.body).to match(u2f_device.name)
expect(page.body).to match('Your U2F device was registered')
end
@@ -44,23 +57,39 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
visit profile_account_path
# First device
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ manage_two_factor_authentication
+ first_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
# Second device
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ second_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
- click_on 'Manage Two-Factor Authentication'
- expect(page.body).to match('You have 2 U2F devices registered')
+
+ expect(page.body).to match(first_device.name)
+ expect(page.body).to match(second_device.name)
+ expect(U2fRegistration.count).to eq(2)
+ end
+
+ it 'allows deleting a device' do
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page.body).to match("You've already enabled two-factor authentication using mobile")
+
+ first_u2f_device = register_u2f_device
+ second_u2f_device = register_u2f_device
+
+ click_on "Delete", match: :first
+
+ expect(page.body).to match('Successfully deleted')
+ expect(page.body).not_to match(first_u2f_device.name)
+ expect(page.body).to match(second_u2f_device.name)
end
end
it 'allows the same device to be registered for multiple users' do
# First user
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
u2f_device = register_u2f_device
expect(page.body).to match('Your U2F device was registered')
logout
@@ -69,7 +98,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
user = login_as(:user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device(u2f_device)
expect(page.body).to match('Your U2F device was registered')
@@ -79,7 +108,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
context "when there are form errors" do
it "doesn't register the device if there are errors" do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
# Have the "u2f device" respond with bad data
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -94,7 +123,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
it "allows retrying registration" do
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
# Failed registration
page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };")
@@ -120,7 +149,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_as(user)
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
@u2f_device = register_u2f_device
logout
end
@@ -159,7 +188,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
current_user = login_as(:user)
current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device
logout
@@ -180,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
current_user = login_as(:user)
current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
+ manage_two_factor_authentication
register_u2f_device(@u2f_device)
logout
@@ -198,7 +227,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do
- unregistered_device = FakeU2fDevice.new(page)
+ unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device"
@@ -208,21 +237,53 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page.body).to match('Authentication via U2F device failed')
end
end
- end
- describe "when two-factor authentication is disabled" do
- let(:user) { create(:user) }
+ describe "when more than one device has been registered by the same user" do
+ it "allows logging in with either device" do
+ # Register first device
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_two_factor_auth_path
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ first_device = register_u2f_device
+
+ # Register second device
+ visit profile_two_factor_auth_path
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ second_device = register_u2f_device
+ logout
+
+ # Authenticate as both devices
+ [first_device, second_device].each do |device|
+ login_as(user)
+ device.respond_to_u2f_authentication
+ click_on "Login Via U2F Device"
+ expect(page.body).to match('We heard back from your U2F device')
+ click_on "Authenticate via U2F Device"
- before do
- login_as(user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- click_on 'Manage Two-Factor Authentication'
- register_u2f_device
+ expect(page.body).to match('Signed in successfully')
+
+ logout
+ end
+ end
end
- it "deletes u2f registrations" do
- expect { click_on "Disable" }.to change { U2fRegistration.count }.from(1).to(0)
+ describe "when two-factor authentication is disabled" do
+ let(:user) { create(:user) }
+
+ before do
+ user = login_as(:user)
+ user.update_attribute(:otp_required_for_login, true)
+ visit profile_account_path
+ manage_two_factor_authentication
+ expect(page).to have_content("Your U2F device needs to be set up.")
+ register_u2f_device
+ end
+
+ it "deletes u2f registrations" do
+ visit profile_account_path
+ expect { click_on "Disable" }.to change { U2fRegistration.count }.by(-1)
+ end
end
end
end
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index a2b8f7b6931..d7880d5778f 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -13,13 +13,13 @@ describe 'Project variables', js: true do
visit namespace_project_variables_path(project.namespace, project)
end
- it 'should show list of variables' do
+ it 'shows list of variables' do
page.within('.variables-table') do
expect(page).to have_content(variable.key)
end
end
- it 'should add new variable' do
+ it 'adds new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
@@ -29,7 +29,7 @@ describe 'Project variables', js: true do
end
end
- it 'should delete variable' do
+ it 'deletes variable' do
page.within('.variables-table') do
find('.btn-variable-delete').click
end
@@ -37,11 +37,12 @@ describe 'Project variables', js: true do
expect(page).not_to have_selector('variables-table')
end
- it 'should edit variable' do
+ it 'edits variable' do
page.within('.variables-table') do
find('.btn-variable-edit').click
end
+ expect(page).to have_content('Update variable')
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Save variable')
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
new file mode 100644
index 00000000000..6fce11de30f
--- /dev/null
+++ b/spec/finders/branches_finder_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe BranchesFinder do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ describe '#execute' do
+ context 'sort only' do
+ it 'sorts by name' do
+ branches_finder = described_class.new(repository, {})
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq("'test'")
+ end
+
+ it 'sorts by recently_updated' do
+ branches_finder = described_class.new(repository, { sort: 'updated_desc' })
+
+ result = branches_finder.execute
+
+ recently_updated_branch = repository.branches.max do |a, b|
+ repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+ end
+
+ expect(result.first.name).to eq(recently_updated_branch.name)
+ end
+
+ it 'sorts by last_updated' do
+ branches_finder = described_class.new(repository, { sort: 'updated_asc' })
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature')
+ end
+ end
+
+ context 'filter only' do
+ it 'filters branches by name' do
+ branches_finder = described_class.new(repository, { search: 'fix' })
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('fix')
+ expect(result.count).to eq(1)
+ end
+
+ it 'does not find any branch with that name' do
+ branches_finder = described_class.new(repository, { search: 'random' })
+
+ result = branches_finder.execute
+
+ expect(result.count).to eq(0)
+ end
+ end
+
+ context 'filter and sort' do
+ it 'filters branches by name and sorts by recently_updated' do
+ params = { sort: 'updated_desc', search: 'feature' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature_conflict')
+ expect(result.count).to eq(2)
+ end
+
+ it 'filters branches by name and sorts by last_updated' do
+ params = { sort: 'updated_asc', search: 'feature' }
+ branches_finder = described_class.new(repository, params)
+
+ result = branches_finder.execute
+
+ expect(result.first.name).to eq('feature')
+ expect(result.count).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index bc385fd0d69..535aabfc18d 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -18,13 +18,13 @@ describe MergeRequestsFinder do
end
describe "#execute" do
- it 'should filter by scope' 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)
end
- it 'should filter by project' do
+ it 'filters by project' do
params = { project_id: project1.id, scope: 'authored', state: 'opened' }
merge_requests = MergeRequestsFinder.new(user, params).execute
expect(merge_requests.size).to eq(1)
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
new file mode 100644
index 00000000000..fdce4e714ff
--- /dev/null
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe MoveToProjectFinder do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:no_access_project) { create(:project) }
+ let(:guest_project) { create(:project) }
+ let(:reporter_project) { create(:project) }
+ let(:developer_project) { create(:project) }
+ let(:master_project) { create(:project) }
+
+ subject { described_class.new(user) }
+
+ describe '#execute' do
+ context 'filter' do
+ it 'does not return projects under Gitlab::Access::REPORTER' do
+ guest_project.team << [user, :guest]
+
+ expect(subject.execute(project)).to be_empty
+ end
+
+ it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project, reporter_project])
+ end
+
+ it 'does not include the source project' do
+ project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to be_empty
+ end
+
+ it 'does not return archived projects' do
+ reporter_project.team << [user, :reporter]
+ reporter_project.update_attributes(archived: true)
+ other_reporter_project = create(:project)
+ other_reporter_project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ end
+
+ it 'does not return projects for which issues are disabled' do
+ reporter_project.team << [user, :reporter]
+ reporter_project.update_attributes(issues_enabled: false)
+ other_reporter_project = create(:project)
+ other_reporter_project.team << [user, :reporter]
+
+ expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ end
+
+ it 'returns a page of projects ordered by id in descending order' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+ end
+
+ it 'returns projects after the given offset id' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
+ end
+ end
+
+ context 'search' do
+ it 'uses Project#search' do
+ expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all }
+
+ subject.execute(project, search: 'wadus')
+ end
+
+ it 'returns projects matching a search query' do
+ foo_project = create(:project)
+ foo_project.team << [user, :master]
+
+ wadus_project = create(:project, name: 'wadus')
+ wadus_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
+ expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project])
+ end
+ end
+ end
+end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 1bd354815e4..7c6860372cc 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -11,7 +11,7 @@ describe NotesFinder do
project.team << [user, :master]
end
- describe :execute do
+ describe '#execute' do
let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
before do
@@ -19,12 +19,12 @@ describe NotesFinder do
note2
end
- it 'should find all notes' do
+ it 'finds all notes' do
notes = NotesFinder.new.execute(project, user, params)
expect(notes.size).to eq(2)
end
- it 'should raise an exception for an invalid target_type' do
+ 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')
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 0a1cc3b3df7..7a3a74335e8 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -23,73 +23,36 @@ describe ProjectsFinder do
let(:finder) { described_class.new }
- describe 'without a group' do
- describe 'without a user' do
- subject { finder.execute }
+ describe 'without a user' do
+ subject { finder.execute }
- it { is_expected.to eq([public_project]) }
- end
-
- describe 'with a user' do
- subject { finder.execute(user) }
-
- describe 'without private projects' do
- it { is_expected.to eq([public_project, internal_project]) }
- end
-
- describe 'with private projects' do
- before do
- private_project.team.add_user(user, Gitlab::Access::MASTER)
- end
-
- it do
- is_expected.to eq([public_project, internal_project,
- private_project])
- end
- end
- end
+ it { is_expected.to eq([public_project]) }
end
- describe 'with a group' do
- describe 'without a user' do
- subject { finder.execute(nil, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user) }
- it { is_expected.to eq([public_project]) }
+ describe 'without private projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
end
- describe 'with a user' do
- subject { finder.execute(user, group: group) }
-
- describe 'without shared projects' do
- it { is_expected.to eq([public_project, internal_project]) }
+ describe 'with private projects' do
+ before do
+ private_project.team.add_user(user, Gitlab::Access::MASTER)
end
- describe 'with shared projects and group membership' do
- before do
- group.add_user(user, Gitlab::Access::DEVELOPER)
-
- shared_project.project_group_links.
- create(group_access: Gitlab::Access::MASTER, group: group)
- end
-
- it do
- is_expected.to eq([shared_project, public_project, internal_project])
- end
+ it do
+ is_expected.to eq([public_project, internal_project, private_project])
end
+ end
+ end
- describe 'with shared projects and project membership' do
- before do
- shared_project.team.add_user(user, Gitlab::Access::DEVELOPER)
+ describe 'with project_ids_relation' do
+ let(:project_ids_relation) { Project.where(id: internal_project.id) }
- shared_project.project_group_links.
- create(group_access: Gitlab::Access::MASTER, group: group)
- end
+ subject { finder.execute(user, project_ids_relation) }
- it do
- is_expected.to eq([shared_project, public_project, internal_project])
- end
- end
- end
+ it { is_expected.to eq([internal_project]) }
end
end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 00000000000..f7e7e733cf7
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:finder) { described_class }
+
+ before { project.team << [user, :developer] }
+
+ describe '#sort' do
+ context 'by date' do
+ let!(:todo1) { create(:todo, user: user, project: project) }
+ let!(:todo2) { create(:todo, user: user, project: project) }
+ let!(:todo3) { create(:todo, user: user, project: project) }
+
+ it 'sorts with oldest created first' do
+ todos = finder.new(user, { sort: 'id_asc' }).execute
+
+ expect(todos.first).to eq(todo1)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo3)
+ end
+
+ it 'sorts with newest created first' do
+ todos = finder.new(user, { sort: 'id_desc' }).execute
+
+ expect(todos.first).to eq(todo3)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo1)
+ end
+ end
+
+ it "sorts by priority" do
+ label_1 = create(:label, title: 'label_1', project: project, priority: 1)
+ label_2 = create(:label, title: 'label_2', project: project, priority: 2)
+ label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ merge_request_1 = create(:merge_request, source_project: project)
+
+ merge_request_1.labels << label_1
+
+ # Covers the case where Todo has more than one label
+ issue_3.labels << label_1
+ issue_3.labels << label_3
+
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ todo_1 = create(:todo, user: user, project: project, target: issue_4)
+ todo_2 = create(:todo, user: user, project: project, target: issue_2)
+ todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+ todo_4 = create(:todo, user: user, project: project, target: issue_1)
+ todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ todos = finder.new(user, { sort: 'priority' }).execute
+
+ expect(todos.first).to eq(todo_3)
+ expect(todos.second).to eq(todo_5)
+ expect(todos.third).to eq(todo_4)
+ expect(todos.fourth).to eq(todo_2)
+ expect(todos.fifth).to eq(todo_1)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
new file mode 100644
index 00000000000..532ebb9640e
--- /dev/null
+++ b/spec/fixtures/api/schemas/issue.json
@@ -0,0 +1,48 @@
+{
+ "type": "object",
+ "required" : [
+ "iid",
+ "title",
+ "confidential"
+ ],
+ "properties" : {
+ "iid": { "type": "integer" },
+ "title": { "type": "string" },
+ "confidential": { "type": "boolean" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+ }
+ },
+ "assignee": {
+ "id": { "type": "integet" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "avatar_url": { "type": "uri" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
new file mode 100644
index 00000000000..0d2067f704a
--- /dev/null
+++ b/spec/fixtures/api/schemas/issues.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "issue.json" }
+}
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
new file mode 100644
index 00000000000..f070fa3b254
--- /dev/null
+++ b/spec/fixtures/api/schemas/list.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "required" : [
+ "id",
+ "list_type",
+ "title",
+ "position"
+ ],
+ "properties" : {
+ "id": { "type": "integer" },
+ "list_type": {
+ "type": "string",
+ "enum": ["backlog", "label", "done"]
+ },
+ "label": {
+ "type": ["object"],
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
+ }
+ },
+ "title": { "type": "string" },
+ "position": { "type": ["integer", "null"] }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/lists.json b/spec/fixtures/api/schemas/lists.json
new file mode 100644
index 00000000000..9f618aa9de5
--- /dev/null
+++ b/spec/fixtures/api/schemas/lists.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "list.json" }
+}
diff --git a/spec/fixtures/config/redis_new_format_host.yml b/spec/fixtures/config/redis_new_format_host.yml
new file mode 100644
index 00000000000..13772677a45
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_host.yml
@@ -0,0 +1,29 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: localhost
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
+test:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: localhost
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
+production:
+ url: redis://:mynewpassword@localhost:6379/99
+ sentinels:
+ -
+ host: slave1
+ port: 26380 # point to sentinel, not to redis port
+ -
+ host: slave2
+ port: 26381 # point to sentinel, not to redis port
diff --git a/spec/fixtures/config/redis_new_format_socket.yml b/spec/fixtures/config/redis_new_format_socket.yml
new file mode 100644
index 00000000000..4e76830c281
--- /dev/null
+++ b/spec/fixtures/config/redis_new_format_socket.yml
@@ -0,0 +1,6 @@
+development:
+ url: unix:/path/to/redis.sock
+test:
+ url: unix:/path/to/redis.sock
+production:
+ url: unix:/path/to/redis.sock
diff --git a/spec/fixtures/config/redis_old_format_host.yml b/spec/fixtures/config/redis_old_format_host.yml
new file mode 100644
index 00000000000..253d0a994f5
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_host.yml
@@ -0,0 +1,5 @@
+# redis://[:password@]host[:port][/db-number][?option=value]
+# more details: http://www.iana.org/assignments/uri-schemes/prov/redis
+development: redis://:mypassword@localhost:6379/99
+test: redis://:mypassword@localhost:6379/99
+production: redis://:mypassword@localhost:6379/99
diff --git a/spec/fixtures/config/redis_old_format_socket.yml b/spec/fixtures/config/redis_old_format_socket.yml
new file mode 100644
index 00000000000..fd31ce8ea3d
--- /dev/null
+++ b/spec/fixtures/config/redis_old_format_socket.yml
@@ -0,0 +1,3 @@
+development: unix:/path/to/old/redis.sock
+test: unix:/path/to/old/redis.sock
+production: unix:/path/to/old/redis.sock
diff --git a/spec/fixtures/domain_blacklist.txt b/spec/fixtures/domain_blacklist.txt
new file mode 100644
index 00000000000..baeb11eda9a
--- /dev/null
+++ b/spec/fixtures/domain_blacklist.txt
@@ -0,0 +1,3 @@
+example.com
+test.com
+foo.bar \ No newline at end of file
diff --git a/spec/fixtures/emails/commands_in_reply.eml b/spec/fixtures/emails/commands_in_reply.eml
new file mode 100644
index 00000000000..06bf60ab734
--- /dev/null
+++ b/spec/fixtures/emails/commands_in_reply.eml
@@ -0,0 +1,43 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+Cool!
+
+/close
+/todo
+/due tomorrow
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/commands_only_reply.eml b/spec/fixtures/emails/commands_only_reply.eml
new file mode 100644
index 00000000000..aed64224b06
--- /dev/null
+++ b/spec/fixtures/emails/commands_only_reply.eml
@@ -0,0 +1,41 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+/close
+/todo
+/due tomorrow
+
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/fixtures/emails/valid_new_issue.eml b/spec/fixtures/emails/valid_new_issue.eml
new file mode 100644
index 00000000000..3cf53a656a5
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue.eml
@@ -0,0 +1,23 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+The reply by email functionality should be extended to allow creating a new issue by email.
+
+* Allow an admin to specify which project the issue should be created under by checking the sender domain.
+* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under.
diff --git a/spec/fixtures/emails/valid_new_issue_empty.eml b/spec/fixtures/emails/valid_new_issue_empty.eml
new file mode 100644
index 00000000000..fc1d52a3f42
--- /dev/null
+++ b/spec/fixtures/emails/valid_new_issue_empty.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_authentication_token.eml
new file mode 100644
index 00000000000..0994c2f7775
--- /dev/null
+++ b/spec/fixtures/emails/wrong_authentication_token.eml
@@ -0,0 +1,18 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+bad_token@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+Subject: New Issue by email
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_mail_key.eml
index 491e078fb5b..491e078fb5b 100644
--- a/spec/fixtures/emails/wrong_reply_key.eml
+++ b/spec/fixtures/emails/wrong_mail_key.eml
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index c75d28d9801..f3e7c2d1a9f 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -256,3 +256,7 @@ However the wrapping tags can not be mixed as such -
- [+ additions +}
- {- delletions -]
- [- delletions -}
+
+### Videos
+
+![My Video](/assets/videos/gitlab-demo.mp4)
diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml
deleted file mode 100644
index 7d01183e3ef..00000000000
--- a/spec/fixtures/parallel_diff_result.yml
+++ /dev/null
@@ -1,820 +0,0 @@
----
-- :left:
- :type: match
- :number: 6
- :text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: match
- :number: 6
- :text: "@@ -6,12 +6,18 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 6
- :text: |2
- <span id="LC6" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 6
- :new_line: 6
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 6
- :text: |2
- <span id="LC6" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 6
- :new_line: 6
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 7
- :text: |2
- <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 7
- :new_line: 7
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 7
- :text: |2
- <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 7
- :new_line: 7
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 8
- :text: |2
- <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 8
- :new_line: 8
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 8
- :text: |2
- <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 8
- :new_line: 8
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: old
- :number: 9
- :text: |
- -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 9
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 9
- :text: |
- +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 9
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 10
- :text: |2
- <span id="LC10" class="line"> <span class="k">end</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 10
- :new_line: 10
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 10
- :text: |2
- <span id="LC10" class="line"> <span class="k">end</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 10
- :new_line: 10
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 11
- :text: |2
- <span id="LC11" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 11
- :new_line: 11
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 11
- :text: |2
- <span id="LC11" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 11
- :new_line: 11
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 12
- :text: |2
- <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 12
- :new_line: 12
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 12
- :text: |2
- <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 12
- :new_line: 12
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: old
- :number: 13
- :text: |
- -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 13
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: old
- :number:
- :text: ''
- :line_code:
- :position:
-- :left:
- :type: old
- :number: 14
- :text: |
- -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 14
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 13
- :text: |
- +<span id="LC13" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 13
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 14
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 14
- :text: |
- +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 14
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 15
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 15
- :text: |
- +<span id="LC15" class="line"> <span class="s2">&quot;PWD&quot;</span> <span class="o">=&gt;</span> <span class="n">path</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 15
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 16
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 16
- :text: |
- +<span id="LC16" class="line"> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 16
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 17
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 17
- :text: |
- +<span id="LC17" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 17
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 18
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 18
- :text: |
- +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 18
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 19
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 19
- :text: |
- +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 19
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 20
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 20
- :text: |
- +<span id="LC20" class="line"> <span class="p">}</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 20
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 15
- :text: |2
- <span id="LC21" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 15
- :new_line: 21
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 21
- :text: |2
- <span id="LC21" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 15
- :new_line: 21
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 16
- :text: |2
- <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 16
- :new_line: 22
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 22
- :text: |2
- <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 16
- :new_line: 22
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 17
- :text: |2
- <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 17
- :new_line: 23
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 23
- :text: |2
- <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 17
- :new_line: 23
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type: match
- :number: 19
- :text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: match
- :number: 25
- :text: "@@ -19,6 +25,7 @@ module Popen"
- :line_code:
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line:
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 19
- :text: |2
- <span id="LC25" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 19
- :new_line: 25
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 25
- :text: |2
- <span id="LC25" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 19
- :new_line: 25
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 20
- :text: |2
- <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 20
- :new_line: 26
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 26
- :text: |2
- <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">&quot;&quot;</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 20
- :new_line: 26
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 21
- :text: |2
- <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 21
- :new_line: 27
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 27
- :text: |2
- <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 21
- :new_line: 27
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number:
- :text: ''
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 28
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type: new
- :number: 28
- :text: |
- +<span id="LC28" class="line"></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line:
- :new_line: 28
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 22
- :text: |2
- <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 22
- :new_line: 29
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 29
- :text: |2
- <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 22
- :new_line: 29
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 23
- :text: |2
- <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 23
- :new_line: 30
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 30
- :text: |2
- <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 23
- :new_line: 30
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
-- :left:
- :type:
- :number: 24
- :text: |2
- <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 24
- :new_line: 31
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
- :right:
- :type:
- :number: 31
- :text: |2
- <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o">&lt;&lt;</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span>
- :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31
- :position: !ruby/object:Gitlab::Diff::Position
- attributes:
- :old_path: files/ruby/popen.rb
- :new_path: files/ruby/popen.rb
- :old_line: 24
- :new_line: 31
- :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
- :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d
diff --git a/spec/fixtures/project_services/campfire/rooms.json b/spec/fixtures/project_services/campfire/rooms.json
new file mode 100644
index 00000000000..71e9645c955
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms.json
@@ -0,0 +1,22 @@
+{
+ "rooms": [
+ {
+ "name": "test-room",
+ "locked": false,
+ "created_at": "2009/01/07 20:43:11 +0000",
+ "updated_at": "2009/03/18 14:31:39 +0000",
+ "topic": "The room topic\n",
+ "id": 123,
+ "membership_limit": 4
+ },
+ {
+ "name": "another room",
+ "locked": true,
+ "created_at": "2009/03/18 14:30:42 +0000",
+ "updated_at": "2013/01/27 14:14:27 +0000",
+ "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+ "id": 456,
+ "membership_limit": 4
+ }
+ ]
+}
diff --git a/spec/fixtures/project_services/campfire/rooms2.json b/spec/fixtures/project_services/campfire/rooms2.json
new file mode 100644
index 00000000000..3d5f635d8b3
--- /dev/null
+++ b/spec/fixtures/project_services/campfire/rooms2.json
@@ -0,0 +1,22 @@
+{
+ "rooms": [
+ {
+ "name": "test-room-not-found",
+ "locked": false,
+ "created_at": "2009/01/07 20:43:11 +0000",
+ "updated_at": "2009/03/18 14:31:39 +0000",
+ "topic": "The room topic\n",
+ "id": 123,
+ "membership_limit": 4
+ },
+ {
+ "name": "another room",
+ "locked": true,
+ "created_at": "2009/03/18 14:30:42 +0000",
+ "updated_at": "2013/01/27 14:14:27 +0000",
+ "topic": "Comment, ideas, GitHub notifications for eCommittee App",
+ "id": 456,
+ "membership_limit": 4
+ }
+ ]
+}
diff --git a/spec/fixtures/video_sample.mp4 b/spec/fixtures/video_sample.mp4
new file mode 100644
index 00000000000..acd45190998
--- /dev/null
+++ b/spec/fixtures/video_sample.mp4
Binary files differ
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index bb28866f010..73f5470cf35 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -54,7 +54,7 @@ describe ApplicationHelper do
describe 'project_icon' do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
project = create(:project, avatar: File.open(avatar_file_path))
avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
@@ -62,7 +62,7 @@ describe ApplicationHelper do
to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
- it 'should give uploaded icon when present' do
+ it 'gives uploaded icon when present' do
project = create(:project)
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
@@ -76,14 +76,14 @@ describe ApplicationHelper do
describe 'avatar_icon' do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user.email).to_s).
to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
end
- it 'should return an url for the avatar with relative url' do
+ it 'returns an url for the avatar with relative url' do
stub_config_setting(relative_url_root: '/gitlab')
# Must be stubbed after the stub above, and separately
stub_config_setting(url: Settings.send(:build_gitlab_url))
@@ -94,14 +94,14 @@ describe ApplicationHelper do
to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
end
- it 'should call gravatar_icon when no User exists with the given email' do
+ it 'calls gravatar_icon when no User exists with the given email' do
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
helper.avatar_icon('foo@example.com', 20, 2)
end
describe 'using a User' do
- it 'should return an URL for the avatar' do
+ it 'returns an URL for the avatar' do
user = create(:user, avatar: File.open(avatar_file_path))
expect(helper.avatar_icon(user).to_s).
@@ -146,7 +146,7 @@ describe ApplicationHelper do
to match('https://secure.gravatar.com')
end
- it 'should return custom gravatar path when gravatar_url is set' do
+ it 'returns custom gravatar path when gravatar_url is set' do
stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
expect(gravatar_icon(user_email, 20)).
@@ -218,12 +218,12 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
- expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending'
+ expect(element.attr('class')).to eq 'js-timeago js-timeago-pending'
end
it 'accepts a custom html_class' do
expect(element(html_class: 'custom_class').attr('class')).
- to eq 'custom_class js-timeago js-timeago-pending'
+ to eq 'js-timeago custom_class js-timeago-pending'
end
it 'accepts a custom tooltip placement' do
@@ -244,6 +244,19 @@ describe ApplicationHelper do
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
+ 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
end
describe 'render_markup' do
@@ -253,19 +266,19 @@ describe ApplicationHelper do
allow(helper).to receive(:current_user).and_return(user)
end
- it 'should preserve encoding' do
+ it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8')
expect(helper.render_markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
- it "should delegate to #markdown when file name corresponds to Markdown" do
+ it "delegates to #markdown when file name corresponds to Markdown" do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown).and_return('NOEL')
expect(helper.render_markup('foo.md', content)).to eq('NOEL')
end
- it "should delegate to #asciidoc when file name corresponds to AsciiDoc" do
+ it "delegates to #asciidoc when file name corresponds to AsciiDoc" do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc).and_return('NOEL')
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 6d1c02db297..a43a7238c70 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BlobHelper do
+ include TreeHelper
+
let(:blob_name) { 'test.lisp' }
let(:no_context_content) { ":type \"assem\"))" }
let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" }
@@ -15,20 +17,20 @@ describe BlobHelper do
end
describe '#highlight' do
- it 'should return plaintext for unknown lexer context' do
- result = helper.highlight(blob_name, no_context_content, nowrap: true)
- expect(result).to eq('<span id="LC1" class="line">:type &quot;assem&quot;))</span>')
+ it 'returns plaintext for unknown lexer context' do
+ result = helper.highlight(blob_name, no_context_content)
+ expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
end
- it 'should highlight single block' do
- expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>]
+ it 'highlights single block' do
+ expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
- expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected)
+ expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end
- it 'should highlight multi-line comments' do
- result = helper.highlight(blob_name, multiline_content, nowrap: true)
+ it 'highlights multi-line comments' do
+ result = helper.highlight(blob_name, multiline_content)
html = Nokogiri::HTML(result)
lines = html.search('.s')
expect(lines.count).to eq(3)
@@ -41,42 +43,66 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do
- %q(<span id="LC1" class="line"><span class="gi">+aaa</span></span>
+ %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
<span id="LC2" class="line"><span class="gi">+bbb</span></span>
<span id="LC3" class="line"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line"> ddd</span>)
+<span id="LC4" class="line"> ddd</span></code></pre>)
end
- it 'should highlight each line properly' do
- result = helper.highlight(blob_name, blob_content, nowrap: true)
+ it 'highlights each line properly' do
+ result = helper.highlight(blob_name, blob_content)
expect(result).to eq(expected)
end
end
end
- describe "#highlighter" do
- it 'should highlight continued blocks' do
- # Both lines have LC1 as ID since formatter doesn't support continue at the moment
- expected = [
- '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>',
- '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">&quot;assem&quot;</span><span class="p">))</span></span>'
- ]
-
- highlighter = helper.highlighter(blob_name, blob_content, nowrap: true)
- result = split_content.map{ |content| highlighter.highlight(content) }
- expect(result).to eq(expected)
- end
- end
-
describe "#sanitize_svg" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
let(:data) { open(input_svg_path).read }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
let(:expected) { open(expected_svg_path).read }
- it 'should retain essential elements' do
+ it 'retains essential elements' do
blob = OpenStruct.new(data: data)
expect(sanitize_svg(blob).data).to eq(expected)
end
end
+
+ describe "#edit_blob_link" do
+ let(:namespace) { create(:namespace, name: 'gitlab' )}
+ let(:project) { create(:project, namespace: namespace) }
+
+ before do
+ allow(self).to receive(:current_user).and_return(double)
+ allow(self).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ it 'verifies blob is text' do
+ expect(helper).not_to receive(:blob_text_viewable?)
+
+ button = edit_blob_link(project, 'refs/heads/master', 'README.md')
+
+ expect(button).to start_with('<button')
+ end
+
+ it 'uses the passed blob instead retrieve from repository' do
+ blob = project.repository.blob_at('refs/heads/master', 'README.md')
+
+ expect(project.repository).not_to receive(:blob_at)
+
+ edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
+ end
+
+ it 'returns a link with the proper route' do
+ link = edit_blob_link(project, 'master', 'README.md')
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md')
+ end
+
+ it 'returns a link with the passed link_opts on the expected route' do
+ link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq('/gitlab/gitlabhq/edit/master/README.md?mr_id=10')
+ end
+ end
end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 45199d0f09d..637b02d9388 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -7,7 +7,13 @@ describe CiStatusHelper do
let(:failed_commit) { double("Ci::Pipeline", status: 'failed') }
describe 'ci_icon_for_status' do
- it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') }
- it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') }
+ it 'renders to correct svg on success' do
+ expect(helper).to receive(:render).with('shared/icons/icon_status_success.svg', anything)
+ helper.ci_icon_for_status(success_commit.status)
+ end
+ it 'renders the correct svg on failure' do
+ expect(helper).to receive(:render).with('shared/icons/icon_status_failed.svg', anything)
+ helper.ci_icon_for_status(failed_commit.status)
+ end
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index e2db33d8345..9c7c79f57c6 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -6,7 +6,7 @@ describe DiffHelper do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_refs) { [commit.parent, commit] }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
@@ -15,70 +15,56 @@ describe DiffHelper do
it 'returns a valid value when cookie is set' do
helper.request.cookies[:diff_view] = 'parallel'
- expect(helper.diff_view).to eq 'parallel'
+ expect(helper.diff_view).to eq :parallel
end
it 'returns a default value when cookie is invalid' do
helper.request.cookies[:diff_view] = 'invalid'
- expect(helper.diff_view).to eq 'inline'
+ expect(helper.diff_view).to eq :inline
end
it 'returns a default value when cookie is nil' do
expect(helper.request.cookies).to be_empty
- expect(helper.diff_view).to eq 'inline'
- end
- end
-
- describe 'diff_hard_limit_enabled?' do
- it 'should return true if param is provided' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(diff_hard_limit_enabled?).to be_truthy
- end
-
- it 'should return false if param is not provided' do
- expect(diff_hard_limit_enabled?).to be_falsey
+ expect(helper.diff_view).to eq :inline
end
end
describe 'diff_options' do
- it 'should return hard limit for a diff if force diff is true' do
- allow(controller).to receive(:params) { { force_show_diff: true } }
- expect(diff_options).to include(Commit.max_diff_options)
+ it 'returns no collapse false' do
+ expect(diff_options).to include(no_collapse: false)
end
- it 'should return safe limit for a diff if force diff is false' do
- expect(diff_options).not_to include(:max_lines, :max_files)
+ it 'returns no collapse true if expand_all_diffs' do
+ allow(controller).to receive(:params) { { expand_all_diffs: true } }
+ expect(diff_options).to include(no_collapse: true)
end
- end
- describe 'unfold_bottom_class' do
- it 'should return empty string when bottom line shouldnt be unfolded' do
- expect(unfold_bottom_class(false)).to eq('')
+ it 'returns no collapse true if action name diff_for_path' do
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options).to include(no_collapse: true)
end
- it 'should return js class when bottom lines should be unfolded' do
- expect(unfold_bottom_class(true)).to eq('js-unfold-bottom')
+ it 'returns paths if action name diff_for_path and param old path' do
+ allow(controller).to receive(:params) { { old_path: 'lib/wadus.rb' } }
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
end
- end
- describe 'unfold_class' do
- it 'returns empty on false' do
- expect(unfold_class(false)).to eq('')
- end
-
- it 'returns a class on true' do
- expect(unfold_class(true)).to eq('unfold js-unfold')
+ it 'returns paths if action name diff_for_path and param new path' do
+ allow(controller).to receive(:params) { { new_path: 'lib/wadus.rb' } }
+ allow(controller).to receive(:action_name) { 'diff_for_path' }
+ expect(diff_options[:paths]).to include('lib/wadus.rb')
end
end
describe '#diff_line_content' do
- it 'should return non breaking space when line is empty' do
+ it 'returns non breaking space when line is empty' do
expect(diff_line_content(nil)).to eq(' &nbsp;')
end
- it 'should return the line itself' do
+ 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')
@@ -99,4 +85,56 @@ describe DiffHelper do
expect(marked_new_line).to be_html_safe
end
end
+
+ describe "#diff_match_line" do
+ let(:old_pos) { 40 }
+ let(:new_pos) { 50 }
+ let(:text) { 'some_text' }
+
+ it "should generate foldable top match line for inline view with empty text by default" do
+ output = diff_match_line old_pos, new_pos
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css "td:nth-child(2):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: ''
+ end
+
+ it "should allow to define text and bottom option" do
+ output = diff_match_line old_pos, new_pos, text: text, bottom: true
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1).diff-line-num.unfold.js-unfold.js-unfold-bottom.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css "td:nth-child(2).diff-line-num.unfold.js-unfold.js-unfold-bottom.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text
+ end
+
+ it "should generate match line for parallel view" do
+ output = diff_match_line old_pos, new_pos, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).to have_css "td:nth-child(3):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text
+ end
+
+ it "should allow to generate only left match line for parallel view" do
+ output = diff_match_line old_pos, nil, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.old_line[data-linenumber='#{old_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).not_to have_css 'td:nth-child(3)'
+ end
+
+ it "should allow to generate only right match line for parallel view" do
+ output = diff_match_line nil, new_pos, text: text, view: :parallel
+
+ expect(output).to be_html_safe
+ expect(output).to have_css "td:nth-child(1):not(.js-unfold-bottom).diff-line-num.unfold.js-unfold.new_line[data-linenumber='#{new_pos}']", text: '...'
+ expect(output).to have_css 'td:nth-child(2).line_content.match.parallel', text: text
+ expect(output).not_to have_css 'td:nth-child(3)'
+ end
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 7a3e38d7e63..3223556e1d3 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -8,37 +8,37 @@ describe EmailsHelper do
end
context 'when time limit is less than 2 hours' do
- it 'should display the time in hours using a singular unit' do
+ it 'displays the time in hours using a singular unit' do
validate_time_string(1.hour, '1 hour')
end
end
context 'when time limit is 2 or more hours' do
- it 'should display the time in hours using a plural unit' do
+ it 'displays the time in hours using a plural unit' do
validate_time_string(2.hours, '2 hours')
end
end
context 'when time limit contains fractions of an hour' do
- it 'should round down to the nearest hour' do
+ it 'rounds down to the nearest hour' do
validate_time_string(96.minutes, '1 hour')
end
end
context 'when time limit is 24 or more hours' do
- it 'should display the time in days using a singular unit' do
+ it 'displays the time in days using a singular unit' do
validate_time_string(24.hours, '1 day')
end
end
context 'when time limit is 2 or more days' do
- it 'should display the time in days using a plural unit' do
+ it 'displays the time in days using a plural unit' do
validate_time_string(2.days, '2 days')
end
end
context 'when time limit contains fractions of a day' do
- it 'should round down to the nearest day' do
+ it 'rounds down to the nearest day' do
validate_time_string(57.hours, '2 days')
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c0d2be98e85..022aba0c0d0 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -6,34 +6,34 @@ describe EventsHelper do
allow(helper).to receive(:current_user).and_return(double)
end
- it 'should display one line of plain text without alteration' do
+ it 'displays one line of plain text without alteration' do
input = 'A short, plain note'
expect(helper.event_note(input)).to match(input)
expect(helper.event_note(input)).not_to match(/\.\.\.\z/)
end
- it 'should display inline code' do
+ it 'displays inline code' do
input = 'A note with `inline code`'
expected = 'A note with <code>inline code</code>'
expect(helper.event_note(input)).to match(expected)
end
- it 'should truncate a note with multiple paragraphs' do
+ it 'truncates a note with multiple paragraphs' do
input = "Paragraph 1\n\nParagraph 2"
expected = 'Paragraph 1...'
expect(helper.event_note(input)).to match(expected)
end
- it 'should display the first line of a code block' do
+ it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
- it 'should truncate a single long line of text' do
+ it 'truncates a single long line of text' do
text = 'The quick brown fox jumped over the lazy dog twice' # 50 chars
input = text * 4
expected = (text * 2).sub(/.{3}/, '...')
@@ -41,7 +41,7 @@ describe EventsHelper do
expect(helper.event_note(input)).to match(expected)
end
- it 'should preserve a link href when link text is truncated' do
+ it 'preserves a link href when link text is truncated' do
text = 'The quick brown fox jumped over the lazy dog' # 44 chars
input = "#{text}#{text}#{text} " # 133 chars
link_url = 'http://example.com/foo/bar/baz' # 30 chars
@@ -52,12 +52,12 @@ describe EventsHelper do
expect(helper.event_note(input)).to match(expected_link_text)
end
- it 'should preserve code color scheme' do
+ it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \
"<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
" <span class=\"s1\">\'hello world\'</span>\n" \
- "<span class=\"k\">end</span>" \
+ "<span class=\"k\">end</span>\n" \
'</code></pre>'
expect(helper.event_note(input)).to eq(expected)
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index ade5c3b02d9..5368e5fab06 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -26,17 +26,17 @@ describe GitlabMarkdownHelper do
describe "referencing multiple objects" do
let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" }
- it "should link to the merge request" do
+ it "links to the merge request" do
expected = namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(helper.markdown(actual)).to match(expected)
end
- it "should link to the commit" do
+ it "links to the commit" do
expected = namespace_project_commit_path(project.namespace, project, commit)
expect(helper.markdown(actual)).to match(expected)
end
- it "should link to the issue" do
+ it "links to the issue" do
expected = namespace_project_issue_path(project.namespace, project, issue)
expect(helper.markdown(actual)).to match(expected)
end
@@ -47,7 +47,7 @@ describe GitlabMarkdownHelper do
let(:second_project) { create(:project, :public) }
let(:second_issue) { create(:issue, project: second_project) }
- it 'should link to the issue' do
+ it 'links to the issue' do
expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue)
expect(markdown(actual, project: second_project)).to match(expected)
end
@@ -58,7 +58,7 @@ describe GitlabMarkdownHelper do
let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
let(:issues) { create_list(:issue, 2, project: project) }
- it 'should handle references nested in links with all the text' do
+ it 'handles references nested in links with all the text' do
actual = helper.link_to_gfm("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real", commit_path)
doc = Nokogiri::HTML.parse(actual)
@@ -88,7 +88,7 @@ describe GitlabMarkdownHelper do
expect(doc.css('a')[4].text).to eq ' for real'
end
- it 'should forward HTML options' do
+ it 'forwards HTML options' do
actual = helper.link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
doc = Nokogiri::HTML.parse(actual)
@@ -110,7 +110,7 @@ describe GitlabMarkdownHelper do
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end
- it 'should replace commit message with emoji to link' 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>)
@@ -125,7 +125,7 @@ describe GitlabMarkdownHelper do
helper.instance_variable_set(:@project_wiki, @wiki)
end
- it "should use Wiki pipeline for markdown files" do
+ it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki, page_slug: "nested/page")
@@ -133,7 +133,7 @@ describe GitlabMarkdownHelper do
helper.render_wiki_content(@wiki)
end
- it "should use Asciidoctor for asciidoc files" do
+ it "uses Asciidoctor for asciidoc files" do
allow(@wiki).to receive(:format).and_return(:asciidoc)
expect(helper).to receive(:asciidoc).with('wiki content')
@@ -141,7 +141,7 @@ describe GitlabMarkdownHelper do
helper.render_wiki_content(@wiki)
end
- it "should use the Gollum renderer for all other file types" do
+ it "uses the Gollum renderer for all other file types" do
allow(@wiki).to receive(:format).and_return(:rdoc)
formatted_content_stub = double('formatted_content')
expect(formatted_content_stub).to receive(:html_safe)
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
index 4acf38771b7..51c49f0e587 100644
--- a/spec/helpers/graph_helper_spec.rb
+++ b/spec/helpers/graph_helper_spec.rb
@@ -6,7 +6,7 @@ describe GraphHelper do
let(:commit) { project.commit("master") }
let(:graph) { Network::Graph.new(project, 'master', commit, '') }
- it 'filter our refs used by GitLab' do
+ it 'filters our refs used by GitLab' do
allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
self.instance_variable_set(:@graph, graph)
refs = get_refs(project.repository, commit)
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 4ea90a80a92..0807534720a 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -4,7 +4,7 @@ describe GroupsHelper do
describe 'group_icon' do
avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
- it 'should return an url for the avatar' do
+ it 'returns an url for the avatar' do
group = create(:group)
group.avatar = File.open(avatar_file_path)
group.save!
@@ -12,7 +12,7 @@ describe GroupsHelper do
to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
end
- it 'should give default avatar_icon when no avatar is present' do
+ it 'gives default avatar_icon when no avatar is present' do
group = create(:group)
group.save!
expect(group_icon(group.path)).to match('group_avatar.png')
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 00000000000..2dd2eab0524
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe IssuablesHelper do
+ let(:label) { build_stubbed(:label) }
+ let(:label2) { build_stubbed(:label) }
+
+ context 'label tooltip' do
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label])).to eq(label.title)
+ end
+
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
+ end
+ end
+end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 831ae7fb69c..67bac782591 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -5,69 +5,24 @@ describe IssuesHelper do
let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project }
- describe "url_for_project_issues" do
- let(:project_url) { ext_project.external_issue_tracker.project_url }
- let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) }
- let(:int_expected) { polymorphic_path([@project.namespace, project]) }
-
- it "should return internal path if used internal tracker" do
- @project = project
- expect(url_for_project_issues).to match(int_expected)
- end
-
- it "should return path to external tracker" do
- @project = ext_project
-
- expect(url_for_project_issues).to match(ext_expected)
- end
-
- it "should return empty string if project nil" do
- @project = nil
-
- expect(url_for_project_issues).to eq ""
- end
-
- it 'returns an empty string if project_url is invalid' do
- expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' }
-
- expect(url_for_project_issues(project)).to eq ''
- end
-
- it 'returns an empty string if project_path is invalid' do
- expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' }
-
- expect(url_for_project_issues(project, only_path: true)).to eq ''
- end
-
- describe "when external tracker was enabled and then config removed" do
- before do
- @project = ext_project
- allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
- end
-
- it "should return path to external tracker" do
- expect(url_for_project_issues).to match(ext_expected)
- end
- end
- end
-
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
- it "should return internal path if used internal tracker" do
+ it "returns internal path if used internal tracker" do
@project = project
+
expect(url_for_issue(issue.iid)).to match(int_expected)
end
- it "should return path to external tracker" do
+ it "returns path to external tracker" do
@project = ext_project
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
- it "should return empty string if project nil" do
+ it "returns empty string if project nil" do
@project = nil
expect(url_for_issue(issue.iid)).to eq ""
@@ -91,66 +46,46 @@ describe IssuesHelper do
allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
end
- it "should return external path" do
+ it "returns external path" do
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
end
end
- describe 'url_for_new_issue' do
- let(:issues_url) { ext_project.external_issue_tracker.new_issue_url }
- let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) }
- let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) }
-
- it "should return internal path if used internal tracker" do
- @project = project
- expect(url_for_new_issue).to match(int_expected)
+ describe "merge_requests_sentence" do
+ subject { merge_requests_sentence(merge_requests)}
+ let(:merge_requests) do
+ [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
+ build(:merge_request, iid: 3)]
end
- it "should return path to external tracker" do
- @project = ext_project
-
- expect(url_for_new_issue).to match(ext_expected)
- end
+ it { is_expected.to eq("!1, !2, or !3") }
+ end
- it "should return empty string if project nil" do
- @project = nil
+ describe '#award_user_list' do
+ let!(:awards) { build_list(:award_emoji, 15) }
- expect(url_for_new_issue).to eq ""
+ it "returns a comma seperated list of 1-9 users" do
+ expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
end
- it 'returns an empty string if issue_url is invalid' do
- expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' }
-
- expect(url_for_new_issue(project)).to eq ''
+ it "displays the current user's name as 'You'" do
+ expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
end
- it 'returns an empty string if issue_path is invalid' do
- expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' }
-
- expect(url_for_new_issue(project, only_path: true)).to eq ''
+ it "truncates lists of larger than 9 users" do
+ expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
end
- describe "when external tracker was enabled and then config removed" do
- before do
- @project = ext_project
- allow(Gitlab.config).to receive(:issues_tracker).and_return(nil)
- end
-
- it "should return internal path" do
- expect(url_for_new_issue).to match(ext_expected)
- end
+ it "displays the current user in front of 0-9 other users" do
+ expect(award_user_list(awards, awards[0].user)).
+ to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
- end
- describe "merge_requests_sentence" do
- subject { merge_requests_sentence(merge_requests)}
- let(:merge_requests) do
- [ build(:merge_request, iid: 1), build(:merge_request, iid: 2),
- build(:merge_request, iid: 3)]
+ it "displays the current user in front regardless of position in the list" do
+ expect(award_user_list(awards, awards[12].user)).
+ to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
end
-
- it { is_expected.to eq("!1, !2, or !3") }
end
describe '#award_active_class' do
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index f75fdb739f6..7998209b7b0 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,54 +9,6 @@ describe MembersHelper do
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
- describe '#default_show_roles' do
- let(:user) { double }
- let(:member) { build(:project_member) }
-
- before do
- allow(helper).to receive(:current_user).and_return(user)
- allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
- allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
- allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
- end
-
- context 'when the current cannot update, destroy or admin the passed member' do
- it 'returns false' do
- expect(helper.default_show_roles(member)).to be_falsy
- end
- end
-
- context 'when the current can update the passed member' do
- before do
- allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
-
- context 'when the current can destroy the passed member' do
- before do
- allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
-
- context 'when the current can admin the passed member source' do
- before do
- allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
- end
-
- it 'returns true' do
- expect(helper.default_show_roles(member)).to be_truthy
- end
- end
- end
-
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb
index 08a93503258..9c577501f00 100644
--- a/spec/helpers/notes_helper_spec.rb
+++ b/spec/helpers/notes_helper_spec.rb
@@ -1,37 +1,30 @@
require "spec_helper"
describe NotesHelper do
- describe "#notes_max_access_for_users" do
- let(:owner) { create(:owner) }
- let(:group) { create(:group) }
- let(:project) { create(:empty_project, namespace: group) }
- let(:master) { create(:user) }
- let(:reporter) { create(:user) }
- let(:guest) { create(:user) }
-
- let(:owner_note) { create(:note, author: owner, project: project) }
- let(:master_note) { create(:note, author: master, project: project) }
- let(:reporter_note) { create(:note, author: reporter, project: project) }
- let!(:notes) { [owner_note, master_note, reporter_note] }
+ let(:owner) { create(:owner) }
+ let(:group) { create(:group) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
- before do
- group.add_owner(owner)
- project.team << [master, :master]
- project.team << [reporter, :reporter]
- project.team << [guest, :guest]
- end
+ let(:owner_note) { create(:note, author: owner, project: project) }
+ let(:master_note) { create(:note, author: master, project: project) }
+ let(:reporter_note) { create(:note, author: reporter, project: project) }
+ let!(:notes) { [owner_note, master_note, reporter_note] }
- it 'return human access levels' do
- original_method = project.team.method(:human_max_access)
- expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args|
- original_method.call(args[1])
- end
+ before do
+ group.add_owner(owner)
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [guest, :guest]
+ end
+ describe "#notes_max_access_for_users" do
+ it 'returns human access levels' do
expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
expect(helper.note_max_access_for_user(master_note)).to eq('Master')
expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter')
- # Call it again to ensure value is cached
- expect(helper.note_max_access_for_user(owner_note)).to eq('Owner')
end
it 'handles access in different projects' do
@@ -43,4 +36,21 @@ describe NotesHelper do
expect(helper.note_max_access_for_user(other_note)).to eq('Reporter')
end
end
+
+ describe '#preload_max_access_for_authors' do
+ before do
+ # This method reads cache from RequestStore, so make sure it's clean.
+ RequestStore.clear!
+ end
+
+ it 'loads multiple users' do
+ expected_access = {
+ owner.id => Gitlab::Access::OWNER,
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER
+ }
+
+ expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access)
+ end
+ end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index cf632f594c7..dc07657e101 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -97,5 +97,14 @@ describe PageLayoutHelper do
expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
end
end
+
+ it 'escapes content' do
+ allow(helper).to receive(:page_card_attributes)
+ .and_return(foo: %q{foo" http-equiv="refresh}.html_safe)
+
+ tags = helper.page_card_meta_tags
+
+ expect(tags).to include(%q{content="foo&quot; http-equiv=&quot;refresh"})
+ end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 604204cca0a..284b58d8d5c 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -136,4 +136,42 @@ describe ProjectsHelper do
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
end
end
+
+ describe '#last_push_event' do
+ let(:user) { double(:user, fork_of: nil) }
+ let(:project) { double(:project, id: 1) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'when there is no current_user' do
+ let(:user) { nil }
+
+ it 'returns nil' do
+ expect(helper.last_push_event).to eq(nil)
+ end
+ end
+
+ it 'returns recent push on the current project' do
+ event = double(:event)
+ expect(user).to receive(:recent_push).with([project.id]).and_return(event)
+
+ expect(helper.last_push_event).to eq(event)
+ end
+
+ context 'when current user has a fork of the current project' do
+ let(:fork) { double(:fork, id: 2) }
+
+ it 'returns recent push considering fork events' do
+ expect(user).to receive(:fork_of).with(project).and_return(fork)
+
+ event_on_fork = double(:event)
+ expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork)
+
+ expect(helper.last_push_event).to eq(event_on_fork)
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 601b6915e27..b0bb991539b 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,7 +42,7 @@ describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
- it "should not include the public group" do
+ it "does not include the public group" do
group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(0)
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index 10121759132..37ac6a2699d 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -17,35 +17,35 @@ describe SubmoduleHelper do
allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure
end
- it 'should detect ssh on standard port' do
+ it 'detects ssh on standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url([ config.user, '@', config.host, ':gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect ssh on non-standard port' do
+ it 'detects ssh on non-standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url([ 'ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect http on standard port' do
+ it 'detects http on standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url([ 'http://', config.host, '/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should detect http on non-standard port' do
+ it 'detects http on non-standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url([ 'http://', config.host, ':3000/gitlab-org/gitlab-ce.git' ].join(''))
expect(submodule_links(submodule_item)).to eq([ namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash') ])
end
- it 'should work with relative_url_root' do
+ it 'works with relative_url_root' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80) # set this just to be sure
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
@@ -55,22 +55,22 @@ describe SubmoduleHelper do
end
context 'submodule on github.com' do
- it 'should detect ssh' do
+ it 'detects ssh' do
stub_url('git@github.com:gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect http' do
+ it 'detects http' do
stub_url('http://github.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect https' do
+ it 'detects https' do
stub_url('https://github.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should return original with non-standard url' do
+ it 'returns original with non-standard url' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
@@ -80,22 +80,22 @@ describe SubmoduleHelper do
end
context 'submodule on gitlab.com' do
- it 'should detect ssh' do
+ it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect http' do
+ it 'detects http' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should detect https' do
+ it 'detects https' do
stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
expect(submodule_links(submodule_item)).to eq([ 'https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash' ])
end
- it 'should return original with non-standard url' do
+ it 'returns original with non-standard url' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
@@ -105,7 +105,7 @@ describe SubmoduleHelper do
end
context 'submodule on unsupported' do
- it 'should return original' do
+ it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 3f62527c5bb..21f35585367 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -1,36 +1,34 @@
require 'spec_helper'
describe TimeHelper do
- describe "#duration_in_words" do
+ describe "#time_interval_in_words" do
it "returns minutes and seconds" do
intervals_in_words = {
100 => "1 minute 40 seconds",
+ 100.32 => "1 minute 40 seconds",
121 => "2 minutes 1 second",
3721 => "62 minutes 1 second",
0 => "0 seconds"
}
intervals_in_words.each do |interval, expectation|
- expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation)
+ expect(time_interval_in_words(interval)).to eq(expectation)
end
end
-
- it "calculates interval from now if there is no finished_at" do
- expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds")
- end
end
- describe "#time_interval_in_words" do
+ describe "#duration_in_numbers" do
it "returns minutes and seconds" do
- intervals_in_words = {
- 100 => "1 minute 40 seconds",
- 121 => "2 minutes 1 second",
- 3721 => "62 minutes 1 second",
- 0 => "0 seconds"
+ durations_and_expectations = {
+ 100 => "01:40",
+ 121 => "02:01",
+ 3721 => "01:02:01",
+ 0 => "00:00",
+ 42 => "00:42"
}
- intervals_in_words.each do |interval, expectation|
- expect(time_interval_in_words(interval)).to eq(expectation)
+ durations_and_expectations.each do |duration, expectation|
+ expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c70dd8076e0..8d6537ba4b5 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -12,7 +12,7 @@ describe TreeHelper do
context "on a directory containing more than one file/directory" do
let(:tree_item) { double(name: "files", path: "files") }
- it "should return the directory name" do
+ it "returns the directory name" do
expect(flatten_tree(tree_item)).to match('files')
end
end
@@ -20,7 +20,7 @@ describe TreeHelper do
context "on a directory containing only one directory" do
let(:tree_item) { double(name: "foo", path: "foo") }
- it "should return the flattened path" do
+ it "returns the flattened path" do
expect(flatten_tree(tree_item)).to match('foo/bar')
end
end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 5178bd130f4..baab30f482f 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -1,41 +1,58 @@
require 'spec_helper'
+require_relative '../../config/initializers/6_validations.rb'
describe '6_validations', lib: true do
+ before :all do
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/c/d')
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/c2')
+ FileUtils.mkdir_p('tmp/tests/paths/a/b/d')
+ end
+
+ after :all do
+ FileUtils.rm_rf('tmp/tests/paths')
+ end
+
context 'with correct settings' do
before do
- mock_storages('foo' => '/a/b/c', 'bar' => 'a/b/d')
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d')
end
it 'passes through' do
- expect { load_validations }.not_to raise_error
+ expect { validate_storages }.not_to raise_error
end
end
context 'with invalid storage names' do
before do
- mock_storages('name with spaces' => '/a/b/c')
+ mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c')
end
it 'throws an error' do
- expect { load_validations }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
+ expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.')
end
end
context 'with nested storage paths' do
before do
- mock_storages('foo' => '/a/b/c', 'bar' => '/a/b/c/d')
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d')
end
it 'throws an error' do
- expect { load_validations }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
+ expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.')
end
end
- def mock_storages(storages)
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ context 'with similar but un-nested storage paths' do
+ before do
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2')
+ end
+
+ it 'passes through' do
+ expect { validate_storages }.not_to raise_error
+ end
end
- def load_validations
- load File.join(__dir__, '../../config/initializers/6_validations.rb')
+ def mock_storages(storages)
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
new file mode 100644
index 00000000000..837b0de9a4c
--- /dev/null
+++ b/spec/initializers/secret_token_spec.rb
@@ -0,0 +1,200 @@
+require 'spec_helper'
+require_relative '../../config/initializers/secret_token'
+
+describe 'create_tokens', lib: true do
+ let(:secrets) { ActiveSupport::OrderedOptions.new }
+
+ before do
+ allow(ENV).to receive(:[]).and_call_original
+ allow(File).to receive(:write)
+ allow(File).to receive(:delete)
+ allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets)
+ allow(Rails).to receive_message_chain(:root, :join) { |string| string }
+ allow(self).to receive(:warn)
+ allow(self).to receive(:exit)
+ end
+
+ context 'setting secret_key_base and otp_key_base' do
+ context 'when none of the secrets exist' do
+ before do
+ allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil)
+ allow(File).to receive(:exist?).with('.secret').and_return(false)
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+ create_tokens
+
+ keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
+
+ expect(keys.uniq).to eq(keys)
+ expect(keys.map(&:length)).to all(eq(128))
+ end
+
+ it 'warns about the secrets to add to secrets.yml' do
+ expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+ expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+ expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+ create_tokens
+ end
+
+ it 'writes the secrets to secrets.yml' do
+ expect(File).to receive(:write).with('config/secrets.yml', any_args) do |filename, contents, options|
+ new_secrets = YAML.load(contents)[Rails.env]
+
+ expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
+ expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
+ expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+ end
+
+ create_tokens
+ end
+
+ it 'does not write a .secret file' do
+ expect(File).not_to receive(:write).with('.secret')
+
+ create_tokens
+ end
+ end
+
+ context 'when the other secrets all exist' do
+ before do
+ secrets.db_key_base = 'db_key_base'
+
+ allow(File).to receive(:exist?).with('.secret').and_return(true)
+ allow(File).to receive(:read).with('.secret').and_return('file_key')
+ end
+
+ context 'when secret_key_base exists in the environment and secrets.yml' do
+ before do
+ allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key')
+ secrets.secret_key_base = 'secret_key_base'
+ secrets.otp_key_base = 'otp_key_base'
+ end
+
+ it 'does not issue a warning' do
+ expect(self).not_to receive(:warn)
+
+ create_tokens
+ end
+
+ it 'uses the environment variable' do
+ create_tokens
+
+ expect(secrets.secret_key_base).to eq('env_key')
+ end
+
+ it 'does not update secrets.yml' do
+ expect(File).not_to receive(:write)
+
+ create_tokens
+ end
+ end
+
+ context 'when secret_key_base and otp_key_base exist' do
+ before do
+ secrets.secret_key_base = 'secret_key_base'
+ secrets.otp_key_base = 'otp_key_base'
+ end
+
+ it 'does not write any files' do
+ expect(File).not_to receive(:write)
+
+ create_tokens
+ end
+
+ it 'sets the the keys to the values from the environment and secrets.yml' do
+ create_tokens
+
+ expect(secrets.secret_key_base).to eq('secret_key_base')
+ expect(secrets.otp_key_base).to eq('otp_key_base')
+ expect(secrets.db_key_base).to eq('db_key_base')
+ end
+
+ it 'deletes the .secret file' do
+ expect(File).to receive(:delete).with('.secret')
+
+ create_tokens
+ end
+ end
+
+ context 'when secret_key_base and otp_key_base do not exist' do
+ before do
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+ allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => secrets.to_h.stringify_keys)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'uses the file secret' do
+ expect(File).to receive(:write) do |filename, contents, options|
+ new_secrets = YAML.load(contents)[Rails.env]
+
+ expect(new_secrets['secret_key_base']).to eq('file_key')
+ expect(new_secrets['otp_key_base']).to eq('file_key')
+ expect(new_secrets['db_key_base']).to eq('db_key_base')
+ end
+
+ create_tokens
+
+ expect(secrets.otp_key_base).to eq('file_key')
+ end
+
+ it 'keeps the other secrets as they were' do
+ create_tokens
+
+ expect(secrets.db_key_base).to eq('db_key_base')
+ end
+
+ it 'warns about the missing secrets' do
+ expect(self).to receive(:warn_missing_secret).with('secret_key_base')
+ expect(self).to receive(:warn_missing_secret).with('otp_key_base')
+
+ create_tokens
+ end
+
+ it 'deletes the .secret file' do
+ expect(File).to receive(:delete).with('.secret')
+
+ create_tokens
+ end
+ end
+ end
+
+ context 'when db_key_base is blank but exists in secrets.yml' do
+ before do
+ secrets.otp_key_base = 'otp_key_base'
+ secrets.secret_key_base = 'secret_key_base'
+ yaml_secrets = secrets.to_h.stringify_keys.merge('db_key_base' => '<%= an_erb_expression %>')
+
+ allow(File).to receive(:exist?).with('.secret').and_return(false)
+ allow(File).to receive(:exist?).with('config/secrets.yml').and_return(true)
+ allow(YAML).to receive(:load_file).with('config/secrets.yml').and_return('test' => yaml_secrets)
+ allow(self).to receive(:warn_missing_secret)
+ end
+
+ it 'warns about updating db_key_base' do
+ expect(self).to receive(:warn_missing_secret).with('db_key_base')
+
+ create_tokens
+ end
+
+ it 'warns about the blank value existing in secrets.yml and exits' do
+ expect(self).to receive(:warn) do |warning|
+ expect(warning).to include('db_key_base')
+ expect(warning).to include('<%= an_erb_expression %>')
+ end
+
+ create_tokens
+ end
+
+ it 'does not update secrets.yml' do
+ expect(self).to receive(:exit).with(1).and_call_original
+ expect(File).not_to receive(:write)
+
+ expect { create_tokens }.to raise_error(SystemExit)
+ end
+ end
+ end
+end
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index 14c8df954a6..290e47763eb 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -17,6 +17,12 @@ describe 'trusted_proxies', lib: true do
expect(request.remote_ip).to eq('10.1.5.89')
expect(request.ip).to eq('10.1.5.89')
end
+
+ it 'filters out bad values' do
+ request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 10.1.5.89')
+ expect(request.remote_ip).to eq('10.1.5.89')
+ expect(request.ip).to eq('10.1.5.89')
+ end
end
context 'with private IP ranges added' do
@@ -41,6 +47,12 @@ describe 'trusted_proxies', lib: true do
expect(request.remote_ip).to eq('1.1.1.1')
expect(request.ip).to eq('1.1.1.1')
end
+
+ it 'handles invalid ip addresses' do
+ request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 1.1.1.1:12345, 1.1.1.1')
+ expect(request.remote_ip).to eq('1.1.1.1')
+ expect(request.ip).to eq('1.1.1.1')
+ end
end
def stub_request(headers = {})
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
new file mode 100644
index 00000000000..6bcfdf191c2
--- /dev/null
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -0,0 +1,41 @@
+/*= require abuse_reports */
+
+/*= require jquery */
+
+((global) => {
+ const FIXTURE = 'abuse_reports.html';
+ const MAX_MESSAGE_LENGTH = 500;
+
+ function assertMaxLength($message) {
+ expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+ }
+
+ describe('Abuse Reports', function() {
+ fixture.preload(FIXTURE);
+
+ beforeEach(function() {
+ fixture.load(FIXTURE);
+ new global.AbuseReports();
+ });
+
+ it('should truncate long messages', function() {
+ const $longMessage = $('#long');
+ expect($longMessage.data('original-message')).toEqual(jasmine.anything());
+ assertMaxLength($longMessage);
+ });
+
+ it('should not truncate short messages', function() {
+ const $shortMessage = $('#short');
+ 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');
+ $longMessage.click();
+ expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
+ $longMessage.click();
+ assertMaxLength($longMessage);
+ });
+ });
+
+})(window.gl);
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
new file mode 100644
index 00000000000..b48026c3b77
--- /dev/null
+++ b/spec/javascripts/application_spec.js
@@ -0,0 +1,32 @@
+
+/*= 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');
+ $button.click(function() {
+ return isClicked = true;
+ });
+ $button.trigger('click');
+ return expect(isClicked).toBe(false);
+ });
+ return it('should be on the same page if a disabled link clicked', function() {
+ var locationBeforeLinkClick;
+ locationBeforeLinkClick = window.location.href;
+ gl.utils.preventDisabledButtons();
+ $('#test-link').click();
+ return expect(window.location.href).toBe(locationBeforeLinkClick);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee
deleted file mode 100644
index 4b6a2bb5440..00000000000
--- a/spec/javascripts/application_spec.js.coffee
+++ /dev/null
@@ -1,30 +0,0 @@
-#= require lib/utils/common_utils
-
-describe 'Application', ->
- describe 'disable buttons', ->
- fixture.preload('application.html')
-
- beforeEach ->
- fixture.load('application.html')
-
- it 'should prevent default action for disabled buttons', ->
-
- gl.utils.preventDisabledButtons()
-
- isClicked = false
- $button = $ '#test-button'
-
- $button.click -> isClicked = true
- $button.trigger 'click'
-
- expect(isClicked).toBe false
-
-
- it 'should be on the same page if a disabled link clicked', ->
-
- locationBeforeLinkClick = window.location.href
- gl.utils.preventDisabledButtons()
-
- $('#test-link').click()
-
- expect(window.location.href).toBe locationBeforeLinkClick
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
new file mode 100644
index 00000000000..c1c12b57b53
--- /dev/null
+++ b/spec/javascripts/awards_handler_spec.js
@@ -0,0 +1,261 @@
+
+/*= require awards_handler */
+
+
+/*= require jquery */
+
+
+/*= require jquery.cookie */
+
+
+/*= require ./fixtures/emoji_menu */
+
+(function() {
+ var awardsHandler, lazyAssert, urlRoot;
+
+ awardsHandler = null;
+
+ window.gl || (window.gl = {});
+
+ window.gon || (window.gon = {});
+
+ gl.emojiAliases = function() {
+ return {
+ '+1': 'thumbsup',
+ '-1': 'thumbsdown'
+ };
+ };
+
+ gon.award_menu_url = '/emojis';
+ urlRoot = gon.relative_url_root;
+
+ lazyAssert = function(done, assertFn) {
+ return setTimeout(function() {
+ assertFn();
+ return done();
+ }, 333);
+ };
+
+ describe('AwardsHandler', function() {
+ fixture.preload('awards_handler.html');
+ beforeEach(function() {
+ fixture.load('awards_handler.html');
+ awardsHandler = new AwardsHandler;
+ spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
+ return function(url, emoji, cb) {
+ return cb();
+ };
+ })(this));
+ spyOn(jQuery, 'get').and.callFake(function(req, cb) {
+ return cb(window.emojiMenu);
+ });
+ spyOn(jQuery, 'cookie');
+ });
+ afterEach(function() {
+ // restore original url root value
+ gon.relative_url_root = urlRoot;
+ });
+ describe('::showEmojiMenu', function() {
+ it('should show emoji menu when Add emoji button clicked', function(done) {
+ $('.js-add-award').eq(0).click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ expect($emojiMenu.length).toBe(1);
+ expect($emojiMenu.hasClass('is-visible')).toBe(true);
+ expect($emojiMenu.find('#emoji_search').length).toBe(1);
+ return expect($('.js-awards-block.current').length).toBe(1);
+ });
+ });
+ it('should also show emoji menu for the smiley icon in notes', function(done) {
+ $('.note-action-button').click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ return expect($emojiMenu.length).toBe(1);
+ });
+ });
+ return it('should remove emoji menu when body is clicked', function(done) {
+ $('.js-add-award').eq(0).click();
+ return lazyAssert(done, function() {
+ var $emojiMenu;
+ $emojiMenu = $('.emoji-menu');
+ $('body').click();
+ expect($emojiMenu.length).toBe(1);
+ expect($emojiMenu.hasClass('is-visible')).toBe(false);
+ return expect($('.js-awards-block.current').length).toBe(0);
+ });
+ });
+ });
+ describe('::addAwardToEmojiBar', function() {
+ it('should add emoji to votes block', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ expect($emojiButton.length).toBe(1);
+ expect($emojiButton.next('.js-counter').text()).toBe('1');
+ return expect($votesBlock.hasClass('hidden')).toBe(false);
+ });
+ it('should remove the emoji when we click again', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ return expect($emojiButton.length).toBe(0);
+ });
+ return it('should decrement the emoji counter', function() {
+ var $emojiButton, $votesBlock;
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton.next('.js-counter').text(5);
+ awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
+ expect($emojiButton.length).toBe(1);
+ return expect($emojiButton.next('.js-counter').text()).toBe('4');
+ });
+ });
+ 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');
+ });
+ });
+ describe('::addAward and ::checkMutuality', function() {
+ return it('should handle :+1: and :-1: mutuality', function() {
+ var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent();
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ expect($thumbsUpEmoji.hasClass('active')).toBe(true);
+ expect($thumbsDownEmoji.hasClass('active')).toBe(false);
+ $thumbsUpEmoji.tooltip();
+ $thumbsDownEmoji.tooltip();
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true);
+ expect($thumbsUpEmoji.hasClass('active')).toBe(false);
+ return expect($thumbsDownEmoji.hasClass('active')).toBe(true);
+ });
+ });
+ describe('::removeEmoji', function() {
+ return it('should remove emoji', function() {
+ var $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
+ expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
+ awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
+ return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
+ });
+ });
+ describe('::addYouToUserList', function() {
+ it('should prepend "You" to the award tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'sam');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam');
+ });
+ });
+ describe('::removeYouToUserList', function() {
+ it('removes "You" from the front of the tooltip', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy');
+ });
+ return it('handles the special case where "You" is not cleanly comma seperated', function() {
+ var $thumbsUpEmoji, $votesBlock, awardUrl;
+ awardUrl = awardsHandler.getAwardUrl();
+ $votesBlock = $('.js-awards-block').eq(0);
+ $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji.attr('data-title', 'You and sam');
+ $thumbsUpEmoji.addClass('active');
+ awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
+ $thumbsUpEmoji.tooltip();
+ return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
+ });
+ });
+ describe('::addEmojiToFrequentlyUsedList', function() {
+ it('should set a cookie with the correct default path', function() {
+ gon.relative_url_root = '';
+ awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
+ path: '/',
+ expires: 365
+ })
+ ;
+ });
+ it('should set a cookie with the correct custom root path', function() {
+ gon.relative_url_root = '/gitlab/subdir';
+ awardsHandler.addEmojiToFrequentlyUsedList('alien');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
+ path: '/gitlab/subdir',
+ expires: 365
+ })
+ ;
+ });
+ });
+ describe('search', function() {
+ return it('should filter the emoji', function() {
+ $('.js-add-award').eq(0).click();
+ expect($('[data-emoji=angel]').is(':visible')).toBe(true);
+ expect($('[data-emoji=anger]').is(':visible')).toBe(true);
+ $('#emoji_search').val('ali').trigger('keyup');
+ expect($('[data-emoji=angel]').is(':visible')).toBe(false);
+ expect($('[data-emoji=anger]').is(':visible')).toBe(false);
+ return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
+ });
+ });
+ return describe('emoji menu', function() {
+ var openEmojiMenuAndAddEmoji, selector;
+ selector = '[data-emoji=sunglasses]';
+ openEmojiMenuAndAddEmoji = function() {
+ var $block, $emoji, $menu;
+ $('.js-add-award').eq(0).click();
+ $menu = $('.emoji-menu');
+ $block = $('.js-awards-block');
+ $emoji = $menu.find(".emoji-menu-list-item " + selector);
+ expect($emoji.length).toBe(1);
+ expect($block.find(selector).length).toBe(0);
+ $emoji.click();
+ expect($menu.hasClass('.is-visible')).toBe(false);
+ return expect($block.find(selector).length).toBe(1);
+ };
+ it('should add selected emoji to awards block', function() {
+ return openEmojiMenuAndAddEmoji();
+ });
+ return it('should remove already selected emoji', function() {
+ var $block, $emoji;
+ openEmojiMenuAndAddEmoji();
+ $('.js-add-award').eq(0).click();
+ $block = $('.js-awards-block');
+ $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector);
+ $emoji.click();
+ return expect($block.find(selector).length).toBe(0);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee
deleted file mode 100644
index d7f9c6fc076..00000000000
--- a/spec/javascripts/awards_handler_spec.js.coffee
+++ /dev/null
@@ -1,200 +0,0 @@
-#= require awards_handler
-#= require jquery
-#= require jquery.cookie
-#= require ./fixtures/emoji_menu
-
-awardsHandler = null
-window.gl or= {}
-window.gon or= {}
-gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' }
-gon.award_menu_url = '/emojis'
-
-
-lazyAssert = (done, assertFn) ->
-
- setTimeout -> # Maybe jasmine.clock here?
- assertFn()
- done()
- , 333
-
-
-describe 'AwardsHandler', ->
-
- fixture.preload 'awards_handler.html'
-
- beforeEach ->
- fixture.load 'awards_handler.html'
- awardsHandler = new AwardsHandler
- spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb()
- spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu
-
-
- describe '::showEmojiMenu', ->
-
- it 'should show emoji menu when Add emoji button clicked', (done) ->
-
- $('.js-add-award').eq(0).click()
-
- lazyAssert done, ->
- $emojiMenu = $ '.emoji-menu'
- expect($emojiMenu.length).toBe 1
- expect($emojiMenu.hasClass('is-visible')).toBe yes
- expect($emojiMenu.find('#emoji_search').length).toBe 1
- expect($('.js-awards-block.current').length).toBe 1
-
-
- it 'should also show emoji menu for the smiley icon in notes', (done) ->
-
- $('.note-action-button').click()
-
- lazyAssert done, ->
- $emojiMenu = $ '.emoji-menu'
- expect($emojiMenu.length).toBe 1
-
-
- it 'should remove emoji menu when body is clicked', (done) ->
-
- $('.js-add-award').eq(0).click()
-
- lazyAssert done, ->
- $emojiMenu = $('.emoji-menu')
- $('body').click()
- expect($emojiMenu.length).toBe 1
- expect($emojiMenu.hasClass('is-visible')).toBe no
- expect($('.js-awards-block.current').length).toBe 0
-
-
- describe '::addAwardToEmojiBar', ->
-
- it 'should add emoji to votes block', ->
-
- $votesBlock = $('.js-awards-block').eq 0
- awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
-
- $emojiButton = $votesBlock.find '[data-emoji=heart]'
-
- expect($emojiButton.length).toBe 1
- expect($emojiButton.next('.js-counter').text()).toBe '1'
- expect($votesBlock.hasClass('hidden')).toBe no
-
-
- it 'should remove the emoji when we click again', ->
-
- $votesBlock = $('.js-awards-block').eq 0
- awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
- awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
- $emojiButton = $votesBlock.find '[data-emoji=heart]'
-
- expect($emojiButton.length).toBe 0
-
-
- it 'should decrement the emoji counter', ->
-
- $votesBlock = $('.js-awards-block').eq 0
- awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
-
- $emojiButton = $votesBlock.find '[data-emoji=heart]'
- $emojiButton.next('.js-counter').text 5
-
- awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no
-
- expect($emojiButton.length).toBe 1
- expect($emojiButton.next('.js-counter').text()).toBe '4'
-
-
- describe '::getAwardUrl', ->
-
- it 'should return the url for request', ->
-
- expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'
-
-
- describe '::addAward and ::checkMutuality', ->
-
- it 'should handle :+1: and :-1: mutuality', ->
-
- awardUrl = awardsHandler.getAwardUrl()
- $votesBlock = $('.js-awards-block').eq 0
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent()
- $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent()
-
- awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no
-
- expect($thumbsUpEmoji.hasClass('active')).toBe yes
- expect($thumbsDownEmoji.hasClass('active')).toBe no
-
- $thumbsUpEmoji.tooltip()
- $thumbsDownEmoji.tooltip()
-
- awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes
-
- expect($thumbsUpEmoji.hasClass('active')).toBe no
- expect($thumbsDownEmoji.hasClass('active')).toBe yes
-
-
- describe '::removeEmoji', ->
-
- it 'should remove emoji', ->
-
- awardUrl = awardsHandler.getAwardUrl()
- $votesBlock = $('.js-awards-block').eq 0
-
- awardsHandler.addAward $votesBlock, awardUrl, 'fire', no
- expect($votesBlock.find('[data-emoji=fire]').length).toBe 1
-
- awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button')
- expect($votesBlock.find('[data-emoji=fire]').length).toBe 0
-
-
- describe 'search', ->
-
- it 'should filter the emoji', ->
-
- $('.js-add-award').eq(0).click()
-
- expect($('[data-emoji=angel]').is(':visible')).toBe yes
- expect($('[data-emoji=anger]').is(':visible')).toBe yes
-
- $('#emoji_search').val('ali').trigger 'keyup'
-
- expect($('[data-emoji=angel]').is(':visible')).toBe no
- expect($('[data-emoji=anger]').is(':visible')).toBe no
- expect($('[data-emoji=alien]').is(':visible')).toBe yes
-
-
- describe 'emoji menu', ->
-
- selector = '[data-emoji=sunglasses]'
-
- openEmojiMenuAndAddEmoji = ->
-
- $('.js-add-award').eq(0).click()
-
- $menu = $ '.emoji-menu'
- $block = $ '.js-awards-block'
- $emoji = $menu.find ".emoji-menu-list-item #{selector}"
-
- expect($emoji.length).toBe 1
- expect($block.find(selector).length).toBe 0
-
- $emoji.click()
-
- expect($menu.hasClass('.is-visible')).toBe no
- expect($block.find(selector).length).toBe 1
-
-
- it 'should add selected emoji to awards block', ->
-
- openEmojiMenuAndAddEmoji()
-
-
- it 'should remove already selected emoji', ->
-
- openEmojiMenuAndAddEmoji()
- $('.js-add-award').eq(0).click()
-
- $block = $ '.js-awards-block'
- $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}"
-
- $emoji.click()
- expect($block.find(selector).length).toBe 0
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
new file mode 100644
index 00000000000..78795f7654a
--- /dev/null
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -0,0 +1,21 @@
+
+/*= require behaviors/autosize */
+
+(function() {
+ describe('Autosize behavior', function() {
+ var load;
+ beforeEach(function() {
+ return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>');
+ });
+ it('does not overwrite the resize property', function() {
+ load();
+ return expect($('textarea')).toHaveCss({
+ resize: 'vertical'
+ });
+ });
+ return load = function() {
+ return $(document).trigger('page:load');
+ };
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee
deleted file mode 100644
index 7fc1d19c35f..00000000000
--- a/spec/javascripts/behaviors/autosize_spec.js.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-#= require behaviors/autosize
-
-describe 'Autosize behavior', ->
- beforeEach ->
- fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>')
-
- it 'does not overwrite the resize property', ->
- load()
- expect($('textarea')).toHaveCss(resize: 'vertical')
-
- load = -> $(document).trigger('page:load')
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
new file mode 100644
index 00000000000..4c52ecd903d
--- /dev/null
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -0,0 +1,93 @@
+
+/*= require behaviors/quick_submit */
+
+(function() {
+ describe('Quick Submit behavior', function() {
+ var keydownEvent;
+ fixture.preload('behaviors/quick_submit.html');
+ beforeEach(function() {
+ fixture.load('behaviors/quick_submit.html');
+ $('form').submit(function(e) {
+ return e.preventDefault();
+ });
+ return this.spies = {
+ submit: spyOnEvent('form', 'submit')
+ };
+ });
+ it('does not respond to other keyCodes', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ keyCode: 32
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('does not respond to Enter alone', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ ctrlKey: false,
+ metaKey: false
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('does not respond to repeated events', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ repeat: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ it('disables submit buttons', function() {
+ $('textarea').trigger(keydownEvent());
+ expect($('input[type=submit]')).toBeDisabled();
+ return expect($('button[type=submit]')).toBeDisabled();
+ });
+ if (navigator.userAgent.match(/Macintosh/)) {
+ it('responds to Meta+Enter', function() {
+ $('input.quick-submit-input').trigger(keydownEvent());
+ return expect(this.spies.submit).toHaveBeenTriggered();
+ });
+ it('excludes other modifier keys', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ altKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ ctrlKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ shiftKey: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ } else {
+ it('responds to Ctrl+Enter', function() {
+ $('input.quick-submit-input').trigger(keydownEvent());
+ return expect(this.spies.submit).toHaveBeenTriggered();
+ });
+ it('excludes other modifier keys', function() {
+ $('input.quick-submit-input').trigger(keydownEvent({
+ altKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ metaKey: true
+ }));
+ $('input.quick-submit-input').trigger(keydownEvent({
+ shiftKey: true
+ }));
+ return expect(this.spies.submit).not.toHaveBeenTriggered();
+ });
+ }
+ return keydownEvent = function(options) {
+ var defaults;
+ if (navigator.userAgent.match(/Macintosh/)) {
+ defaults = {
+ keyCode: 13,
+ metaKey: true
+ };
+ } else {
+ defaults = {
+ keyCode: 13,
+ ctrlKey: true
+ };
+ }
+ return $.Event('keydown', $.extend({}, defaults, options));
+ };
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee
deleted file mode 100644
index d3b003a328a..00000000000
--- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee
+++ /dev/null
@@ -1,70 +0,0 @@
-#= require behaviors/quick_submit
-
-describe 'Quick Submit behavior', ->
- fixture.preload('behaviors/quick_submit.html')
-
- beforeEach ->
- fixture.load('behaviors/quick_submit.html')
-
- # Prevent a form submit from moving us off the testing page
- $('form').submit (e) -> e.preventDefault()
-
- @spies = {
- submit: spyOnEvent('form', 'submit')
- }
-
- it 'does not respond to other keyCodes', ->
- $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'does not respond to Enter alone', ->
- $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'does not respond to repeated events', ->
- $('input.quick-submit-input').trigger(keydownEvent(repeat: true))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- it 'disables submit buttons', ->
- $('textarea').trigger(keydownEvent())
-
- expect($('input[type=submit]')).toBeDisabled()
- expect($('button[type=submit]')).toBeDisabled()
-
- # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
- # only run the tests that apply to the current platform
- if navigator.userAgent.match(/Macintosh/)
- it 'responds to Meta+Enter', ->
- $('input.quick-submit-input').trigger(keydownEvent())
-
- expect(@spies.submit).toHaveBeenTriggered()
-
- it 'excludes other modifier keys', ->
- $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
- $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true))
- $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
- else
- it 'responds to Ctrl+Enter', ->
- $('input.quick-submit-input').trigger(keydownEvent())
-
- expect(@spies.submit).toHaveBeenTriggered()
-
- it 'excludes other modifier keys', ->
- $('input.quick-submit-input').trigger(keydownEvent(altKey: true))
- $('input.quick-submit-input').trigger(keydownEvent(metaKey: true))
- $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true))
-
- expect(@spies.submit).not.toHaveBeenTriggered()
-
- keydownEvent = (options) ->
- if navigator.userAgent.match(/Macintosh/)
- defaults = { keyCode: 13, metaKey: true }
- else
- defaults = { keyCode: 13, ctrlKey: true }
-
- $.Event('keydown', $.extend({}, defaults, options))
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
new file mode 100644
index 00000000000..724c3baf989
--- /dev/null
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -0,0 +1,44 @@
+
+/*= require behaviors/requires_input */
+
+(function() {
+ describe('requiresInput', function() {
+ fixture.preload('behaviors/requires_input.html');
+ beforeEach(function() {
+ return fixture.load('behaviors/requires_input.html');
+ });
+ it('disables submit when any field is required', function() {
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).toBeDisabled();
+ });
+ it('enables submit when no field is required', function() {
+ $('*[required=required]').removeAttr('required');
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ it('enables submit when all required fields are pre-filled', function() {
+ $('*[required=required]').remove();
+ $('.js-requires-input').requiresInput();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ it('enables submit when all required fields receive input', function() {
+ $('.js-requires-input').requiresInput();
+ $('#required1').val('input1').change();
+ expect($('.submit')).toBeDisabled();
+ $('#optional1').val('input1').change();
+ expect($('.submit')).toBeDisabled();
+ $('#required2').val('input2').change();
+ $('#required3').val('input3').change();
+ $('#required4').val('input4').change();
+ $('#required5').val('1').change();
+ return expect($('.submit')).not.toBeDisabled();
+ });
+ return it('is called on page:load event', function() {
+ var spy;
+ spy = spyOn($.fn, 'requiresInput');
+ $(document).trigger('page:load');
+ return expect(spy).toHaveBeenCalled();
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/behaviors/requires_input_spec.js.coffee b/spec/javascripts/behaviors/requires_input_spec.js.coffee
deleted file mode 100644
index 61a17632173..00000000000
--- a/spec/javascripts/behaviors/requires_input_spec.js.coffee
+++ /dev/null
@@ -1,49 +0,0 @@
-#= require behaviors/requires_input
-
-describe 'requiresInput', ->
- fixture.preload('behaviors/requires_input.html')
-
- beforeEach ->
- fixture.load('behaviors/requires_input.html')
-
- it 'disables submit when any field is required', ->
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).toBeDisabled()
-
- it 'enables submit when no field is required', ->
- $('*[required=required]').removeAttr('required')
-
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'enables submit when all required fields are pre-filled', ->
- $('*[required=required]').remove()
-
- $('.js-requires-input').requiresInput()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'enables submit when all required fields receive input', ->
- $('.js-requires-input').requiresInput()
-
- $('#required1').val('input1').change()
- expect($('.submit')).toBeDisabled()
-
- $('#optional1').val('input1').change()
- expect($('.submit')).toBeDisabled()
-
- $('#required2').val('input2').change()
- $('#required3').val('input3').change()
- $('#required4').val('input4').change()
- $('#required5').val('1').change()
-
- expect($('.submit')).not.toBeDisabled()
-
- it 'is called on page:load event', ->
- spy = spyOn($.fn, 'requiresInput')
-
- $(document).trigger('page:load')
-
- expect(spy).toHaveBeenCalled()
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
new file mode 100644
index 00000000000..078e4b00023
--- /dev/null
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -0,0 +1,164 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+(() => {
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ $.cookie('issue_board_welcome_hidden', 'false');
+ });
+
+ describe('Store', () => {
+ it('starts with a blank state', () => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ });
+
+ describe('lists', () => {
+ it('creates new list without persisting to DB', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ });
+
+ it('finds list by ID', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(list.id).toBe(1);
+ });
+
+ it('finds list by type', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('type', 'label');
+
+ expect(list).toBeDefined();
+ });
+
+ it('finds list limited by type', () => {
+ gl.issueBoards.BoardsStore.addList({
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'backlog'
+ });
+ const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog');
+
+ expect(list).toBeDefined();
+ });
+
+ it('gets issue when new list added', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ const list = gl.issueBoards.BoardsStore.findList('id', 1);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ expect(list.issues[0].id).toBe(1);
+ done();
+ }, 0);
+ });
+
+ 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);
+ });
+
+ 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);
+ });
+
+ it('check for blank state adding when backlog & done list exist', () => {
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'backlog'
+ });
+ gl.issueBoards.BoardsStore.addList({
+ list_type: 'done'
+ });
+
+ expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
+ });
+
+ it('adds the blank state', () => {
+ gl.issueBoards.BoardsStore.addBlankState();
+
+ const list = gl.issueBoards.BoardsStore.findList('type', 'blank', 'blank');
+ expect(list).toBeDefined();
+ });
+
+ it('removes list from state', () => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ 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);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveList(listOne, ['2', '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);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(1);
+
+ done();
+ }, 0);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
new file mode 100644
index 00000000000..3569d1b98bd
--- /dev/null
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -0,0 +1,83 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('Issue model', () => {
+ let issue;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [{
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing'
+ }]
+ });
+ });
+
+ it('has label', () => {
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('add new label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+ });
+
+ it('does not add existing label', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'test',
+ color: 'blue',
+ description: 'bugs!'
+ });
+
+ expect(issue.labels.length).toBe(1);
+ });
+
+ it('finds label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ expect(label).toBeDefined();
+ });
+
+ it('removes label', () => {
+ const label = issue.findLabel(issue.labels[0]);
+ issue.removeLabel(label);
+ expect(issue.labels.length).toBe(0);
+ });
+
+ it('removes multiple labels', () => {
+ issue.addLabel({
+ id: 2,
+ title: 'bug',
+ color: 'blue',
+ description: 'bugs!'
+ });
+ expect(issue.labels.length).toBe(2);
+
+ issue.removeLabels([issue.labels[0], issue.labels[1]]);
+ expect(issue.labels.length).toBe(0);
+ });
+});
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
new file mode 100644
index 00000000000..c206b794442
--- /dev/null
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -0,0 +1,89 @@
+//= require jquery
+//= require jquery_ujs
+//= require jquery.cookie
+//= require vue
+//= require vue-resource
+//= require lib/utils/url_utility
+//= require boards/models/issue
+//= require boards/models/label
+//= require boards/models/list
+//= require boards/models/user
+//= require boards/services/board_service
+//= require boards/stores/boards_store
+//= require ./mock_data
+
+describe('List model', () => {
+ let list;
+
+ beforeEach(() => {
+ gl.boardService = new BoardService('/test/issue-boards/board');
+ gl.issueBoards.BoardsStore.create();
+
+ list = new List(listObj);
+ });
+
+ it('gets issues when created', (done) => {
+ setTimeout(() => {
+ expect(list.issues.length).toBe(1);
+ done();
+ }, 0);
+ });
+
+ it('saves list and returns ID', (done) => {
+ list = new List({
+ title: 'test',
+ label: {
+ id: 1,
+ title: 'test',
+ color: 'red'
+ }
+ });
+ list.save();
+
+ setTimeout(() => {
+ expect(list.id).toBe(1);
+ expect(list.type).toBe('label');
+ expect(list.position).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('destroys the list', (done) => {
+ gl.issueBoards.BoardsStore.addList(listObj);
+ list = gl.issueBoards.BoardsStore.findList('id', 1);
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+ list.destroy();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
+ done();
+ }, 0);
+ });
+
+ it('can\'t search when not backlog', () => {
+ expect(list.canSearch()).toBe(false);
+ });
+
+ it('can search when backlog', () => {
+ list.type = 'backlog';
+ expect(list.canSearch()).toBe(true);
+ });
+
+ it('gets issue from list', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(issue).toBeDefined();
+ done();
+ }, 0);
+ });
+
+ it('removes issue', (done) => {
+ setTimeout(() => {
+ const issue = list.findIssue(1);
+ expect(list.issues.length).toBe(1);
+ list.removeIssue(issue);
+ expect(list.issues.length).toBe(0);
+ done();
+ }, 0);
+ });
+});
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
new file mode 100644
index 00000000000..0c37ec8354f
--- /dev/null
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -0,0 +1,53 @@
+const listObj = {
+ id: 1,
+ position: 0,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 1,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const listObjDuplicate = {
+ id: 2,
+ position: 1,
+ title: 'Test',
+ list_type: 'label',
+ label: {
+ id: 2,
+ title: 'Testing',
+ color: 'red',
+ description: 'testing;'
+ }
+};
+
+const BoardsMockData = {
+ 'GET': {
+ '/test/issue-boards/board/lists{/id}/issues': [{
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: []
+ }]
+ },
+ 'POST': {
+ '/test/issue-boards/board/lists{/id}': listObj
+ },
+ 'PUT': {
+ '/test/issue-boards/board/lists{/id}': {}
+ },
+ 'DELETE': {
+ '/test/issue-boards/board/lists{/id}': {}
+ }
+};
+
+Vue.http.interceptors.push((request, next) => {
+ const body = BoardsMockData[request.method][request.url];
+
+ next(request.respondWith(JSON.stringify(body), {
+ status: 200
+ }));
+});
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
new file mode 100644
index 00000000000..8bd113e7d86
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.coffee
@@ -0,0 +1,50 @@
+#= require lib/utils/datetime_utility
+
+describe 'Date time utils', ->
+ describe 'get day name', ->
+ it 'should return Sunday', ->
+ day = gl.utils.getDayName(new Date('07/17/2016'))
+ expect(day).toBe('Sunday')
+
+ it 'should return Monday', ->
+ day = gl.utils.getDayName(new Date('07/18/2016'))
+ expect(day).toBe('Monday')
+
+ it 'should return Tuesday', ->
+ day = gl.utils.getDayName(new Date('07/19/2016'))
+ expect(day).toBe('Tuesday')
+
+ it 'should return Wednesday', ->
+ day = gl.utils.getDayName(new Date('07/20/2016'))
+ expect(day).toBe('Wednesday')
+
+ it 'should return Thursday', ->
+ day = gl.utils.getDayName(new Date('07/21/2016'))
+ expect(day).toBe('Thursday')
+
+ it 'should return Friday', ->
+ day = gl.utils.getDayName(new Date('07/22/2016'))
+ expect(day).toBe('Friday')
+
+ it 'should return Saturday', ->
+ day = gl.utils.getDayName(new Date('07/23/2016'))
+ expect(day).toBe('Saturday')
+
+ describe 'get day difference', ->
+ it 'should return 7', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('07/08/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(7)
+
+ it 'should return 31', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('08/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(31)
+
+ it 'should return 365', ->
+ firstDay = new Date('07/02/2015')
+ secondDay = new Date('07/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(365) \ No newline at end of file
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
new file mode 100644
index 00000000000..22293d4de87
--- /dev/null
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -0,0 +1,122 @@
+//= 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');
+ };
+
+ beforeEach(() => {
+ CommentsStore.state = {};
+ });
+
+ describe('New discussion', () => {
+ it('creates new discussion', () => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ });
+
+ it('creates new note in discussion', () => {
+ createDiscussion();
+ createDiscussion(2);
+
+ const discussion = CommentsStore.state['a'];
+ expect(Object.keys(discussion.notes).length).toBe(2);
+ });
+ });
+
+ describe('Get note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('gets note by ID', () => {
+ const note = CommentsStore.get('a', 1);
+ expect(note).toBeDefined();
+ expect(note.id).toBe(1);
+ });
+ });
+
+ describe('Delete discussion', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('deletes discussion by ID', () => {
+ CommentsStore.delete('a', 1);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+
+ it('deletes discussion when no more notes', () => {
+ createDiscussion();
+ createDiscussion(2);
+ expect(Object.keys(CommentsStore.state).length).toBe(1);
+ expect(Object.keys(CommentsStore.state['a'].notes).length).toBe(2);
+
+ CommentsStore.delete('a', 1);
+ CommentsStore.delete('a', 2);
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ });
+ });
+
+ describe('Update note', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('updates note to be unresolved', () => {
+ CommentsStore.update('a', 1, false, 'test');
+
+ const note = CommentsStore.get('a', 1);
+ expect(note.resolved).toBe(false);
+ });
+ });
+
+ describe('Discussion resolved', () => {
+ beforeEach(() => {
+ expect(Object.keys(CommentsStore.state).length).toBe(0);
+ createDiscussion();
+ });
+
+ it('is resolved with single note', () => {
+ const discussion = CommentsStore.state['a'];
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('is unresolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+ console.log(discussion.isResolved());
+
+ expect(discussion.isResolved()).toBe(false);
+ });
+
+ it('is resolved with 2 notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('resolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2, false);
+
+ discussion.resolveAllNotes();
+ expect(discussion.isResolved()).toBe(true);
+ });
+
+ it('unresolve all notes', () => {
+ const discussion = CommentsStore.state['a'];
+ createDiscussion(2);
+
+ discussion.unResolveAllNotes();
+ expect(discussion.isResolved()).toBe(false);
+ });
+ });
+})();
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
new file mode 100644
index 00000000000..eced2f6575d
--- /dev/null
+++ b/spec/javascripts/extensions/array_spec.js
@@ -0,0 +1,22 @@
+
+/*= 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.coffee b/spec/javascripts/extensions/array_spec.js.coffee
deleted file mode 100644
index 4ceac619422..00000000000
--- a/spec/javascripts/extensions/array_spec.js.coffee
+++ /dev/null
@@ -1,12 +0,0 @@
-#= require extensions/array
-
-describe 'Array extensions', ->
- describe 'first', ->
- it 'returns the first item', ->
- arr = [0, 1, 2, 3, 4, 5]
- expect(arr.first()).toBe(0)
-
- describe 'last', ->
- it 'returns the last item', ->
- arr = [0, 1, 2, 3, 4, 5]
- expect(arr.last()).toBe(5)
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
new file mode 100644
index 00000000000..b644344b95a
--- /dev/null
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -0,0 +1,42 @@
+
+/*= require extensions/jquery */
+
+(function() {
+ describe('jQuery extensions', function() {
+ describe('disable', function() {
+ beforeEach(function() {
+ return fixture.set('<input type="text" />');
+ });
+ it('adds the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveAttr('disabled', 'disabled');
+ });
+ return it('adds the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.disable();
+ return expect($input).toHaveClass('disabled');
+ });
+ });
+ return describe('enable', function() {
+ beforeEach(function() {
+ return fixture.set('<input type="text" disabled="disabled" class="disabled" />');
+ });
+ it('removes the disabled attribute', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveAttr('disabled');
+ });
+ return it('removes the disabled class', function() {
+ var $input;
+ $input = $('input').first();
+ $input.enable();
+ return expect($input).not.toHaveClass('disabled');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee
deleted file mode 100644
index b10e16b7d01..00000000000
--- a/spec/javascripts/extensions/jquery_spec.js.coffee
+++ /dev/null
@@ -1,34 +0,0 @@
-#= require extensions/jquery
-
-describe 'jQuery extensions', ->
- describe 'disable', ->
- beforeEach ->
- fixture.set '<input type="text" />'
-
- it 'adds the disabled attribute', ->
- $input = $('input').first()
-
- $input.disable()
- expect($input).toHaveAttr('disabled', 'disabled')
-
- it 'adds the disabled class', ->
- $input = $('input').first()
-
- $input.disable()
- expect($input).toHaveClass('disabled')
-
- describe 'enable', ->
- beforeEach ->
- fixture.set '<input type="text" disabled="disabled" class="disabled" />'
-
- it 'removes the disabled attribute', ->
- $input = $('input').first()
-
- $input.enable()
- expect($input).not.toHaveAttr('disabled')
-
- it 'removes the disabled class', ->
- $input = $('input').first()
-
- $input.enable()
- expect($input).not.toHaveClass('disabled')
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
new file mode 100644
index 00000000000..2ec302abcb7
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.html.haml
@@ -0,0 +1,16 @@
+.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/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
index d55936ee4f9..1ef2e8f8624 100644
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -39,7 +39,7 @@
%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
+ %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.
diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee
deleted file mode 100644
index ce1a41390d2..00000000000
--- a/spec/javascripts/fixtures/emoji_menu.coffee
+++ /dev/null
@@ -1,957 +0,0 @@
-window.emojiMenu = """
- <div class='emoji-menu'>
- <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" />
- <div class='emoji-menu-content'>
- <h5 class='emoji-menu-title'>
- Emoticons
- </h5>
- <ul class='clearfix emoji-menu-list'>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <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>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div>
- </button>
- </li>
- <li class='pull-left text-center emoji-menu-list-item'>
- <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>
- <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div>
- </button>
- </li>
- </ul>
- </div>
- </div>
-"""
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
new file mode 100644
index 00000000000..99e3f7247bd
--- /dev/null
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -0,0 +1,4 @@
+(function() {
+ window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
+
+}).call(this);
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
new file mode 100644
index 00000000000..397bdc85c67
--- /dev/null
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -0,0 +1,16 @@
+.block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip
+ .title.hide-collapsed
+ %a.edit-link.pull-right{ href: "#" }
+ Edit
+ .selectbox.hide-collapsed{ style: "display: none;" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
+ %span.dropdown-toggle-text
+ Label
+ %i.fa.fa-chevron-down
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
index 470cabeafbb..06c2ab1e823 100644
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ b/spec/javascripts/fixtures/issues_show.html.haml
@@ -1,7 +1,7 @@
:css
.hidden { display: none !important; }
-.flash-container
+.flash-container.flash-container-page
.flash-alert
.flash-notice
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 84e8d0ba1e4..4919d77e5a4 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -1 +1 @@
-[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"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/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
+[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"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/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
new file mode 100644
index 00000000000..dc6231ebb38
--- /dev/null
+++ b/spec/javascripts/issue_spec.js
@@ -0,0 +1,121 @@
+
+/*= require lib/utils/text_utility */
+
+
+/*= require issue */
+
+(function() {
+ describe('Issue', function() {
+ return describe('task lists', function() {
+ fixture.preload('issues_show.html');
+ beforeEach(function() {
+ fixture.load('issues_show.html');
+ return this.issue = new Issue();
+ });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return 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('/foo');
+ return expect(req.data.issue.description).not.toBe(null);
+ });
+ return $('.js-task-list-field').trigger('tasklist:changed');
+ });
+ });
+ });
+
+ describe('reopen/close issue', function() {
+ fixture.preload('issues_show.html');
+ beforeEach(function() {
+ fixture.load('issues_show.html');
+ return this.issue = new Issue();
+ });
+ it('closes an issue', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://gitlab.com/issues/6/close');
+ return req.success({
+ id: 34
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeVisible();
+ expect($btnClose).toBeHidden();
+ expect($('div.status-box-closed')).toBeVisible();
+ return expect($('div.status-box-open')).toBeHidden();
+ });
+ it('fails to close an issue with success:false', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami');
+ return req.success({
+ saved: false
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-closed')).toBeHidden();
+ expect($('div.status-box-open')).toBeVisible();
+ expect($('div.flash-alert')).toBeVisible();
+ return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+ });
+ it('fails to closes an issue with HTTP error', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami');
+ return req.error();
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose.text()).toBe('Close');
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+ $btnClose.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-closed')).toBeHidden();
+ expect($('div.status-box-open')).toBeVisible();
+ expect($('div.flash-alert')).toBeVisible();
+ return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+ });
+ return it('reopens an issue', function() {
+ var $btnClose, $btnReopen;
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe('http://gitlab.com/issues/6/reopen');
+ return req.success({
+ id: 34
+ });
+ });
+ $btnClose = $('a.btn-close');
+ $btnReopen = $('a.btn-reopen');
+ expect($btnReopen.text()).toBe('Reopen');
+ $btnReopen.trigger('click');
+ expect($btnReopen).toBeHidden();
+ expect($btnClose).toBeVisible();
+ expect($('div.status-box-open')).toBeVisible();
+ return expect($('div.status-box-closed')).toBeHidden();
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
deleted file mode 100644
index d84d80f266b..00000000000
--- a/spec/javascripts/issue_spec.js.coffee
+++ /dev/null
@@ -1,109 +0,0 @@
-#= require lib/utils/text_utility
-#= require issue
-
-describe 'Issue', ->
- describe 'task lists', ->
- fixture.preload('issues_show.html')
-
- beforeEach ->
- fixture.load('issues_show.html')
- @issue = new Issue()
-
- it 'modifies the Markdown field', ->
- spyOn(jQuery, 'ajax').and.stub()
- $('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', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PATCH')
- expect(req.url).toBe('/foo')
- expect(req.data.issue.description).not.toBe(null)
-
- $('.js-task-list-field').trigger('tasklist:changed')
-describe 'reopen/close issue', ->
- fixture.preload('issues_show.html')
- beforeEach ->
- fixture.load('issues_show.html')
- @issue = new Issue()
- it 'closes an issue', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://gitlab.com/issues/6/close')
- req.success id: 34
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeVisible()
- expect($btnClose).toBeHidden()
- expect($('div.status-box-closed')).toBeVisible()
- expect($('div.status-box-open')).toBeHidden()
-
- it 'fails to close an issue with success:false', ->
-
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://goesnowhere.nothing/whereami')
- req.success saved: false
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- $btnClose.attr('href','http://goesnowhere.nothing/whereami')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.flash-alert')).toBeVisible()
- expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
-
- it 'fails to closes an issue with HTTP error', ->
-
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://goesnowhere.nothing/whereami')
- req.error()
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- $btnClose.attr('href','http://goesnowhere.nothing/whereami')
- expect($btnReopen).toBeHidden()
- expect($btnClose.text()).toBe('Close')
- expect(typeof $btnClose.prop('disabled')).toBe('undefined')
-
- $btnClose.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.flash-alert')).toBeVisible()
- expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
-
- it 'reopens an issue', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PUT')
- expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
- req.success id: 34
-
- $btnClose = $('a.btn-close')
- $btnReopen = $('a.btn-reopen')
- expect($btnReopen.text()).toBe('Reopen')
-
- $btnReopen.trigger('click')
-
- expect($btnReopen).toBeHidden()
- expect($btnClose).toBeVisible()
- expect($('div.status-box-open')).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden()
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
new file mode 100644
index 00000000000..840c7b6d015
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+//= require lib/utils/type_utility
+//= require jquery
+//= require bootstrap
+//= require gl_dropdown
+//= require select2
+//= require jquery.nicescroll
+//= require api
+//= require create_label
+//= require issuable_context
+//= require users_select
+//= require labels_select
+
+(() => {
+ let saveLabelCount = 0;
+ describe('Issue dropdown sidebar', () => {
+ fixture.preload('issue_sidebar_label.html');
+
+ beforeEach(() => {
+ fixture.load('issue_sidebar_label.html');
+ new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+ new LabelsSelect();
+
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const d = $.Deferred();
+ let LABELS_DATA = []
+
+ if (req.url === '/root/test/labels.json') {
+ for (let i = 0; i < 10; i++) {
+ LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ } else if (req.url === '/root/test/issues/2.json') {
+ let tmp = []
+ for (let i = 0; i < saveLabelCount; i++) {
+ tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ LABELS_DATA = {labels: tmp};
+ }
+
+ d.resolve(LABELS_DATA);
+ return d.promise();
+ });
+ });
+
+ it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+ saveLabelCount = 5;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+ done();
+ }, 0);
+ }, 0);
+ });
+
+ it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+ saveLabelCount = 6;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+})();
+
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
new file mode 100644
index 00000000000..e2789571607
--- /dev/null
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -0,0 +1,229 @@
+
+/*= require line_highlighter */
+
+(function() {
+ describe('LineHighlighter', function() {
+ var clickLine;
+ fixture.preload('line_highlighter.html');
+ clickLine = function(number, eventData) {
+ var e;
+ if (eventData == null) {
+ eventData = {};
+ }
+ if ($.isEmptyObject(eventData)) {
+ return $("#L" + number).mousedown().click();
+ } else {
+ e = $.Event('mousedown', eventData);
+ return $("#L" + number).trigger(e).click();
+ }
+ };
+ beforeEach(function() {
+ fixture.load('line_highlighter.html');
+ this["class"] = new LineHighlighter();
+ this.css = this["class"].highlightClass;
+ return this.spies = {
+ __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {})
+ };
+ });
+ describe('behavior', function() {
+ it('highlights one line given in the URL hash', function() {
+ new LineHighlighter('#L13');
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ it('highlights a range of lines given in the URL hash', function() {
+ var i, line, results;
+ new LineHighlighter('#L5-25');
+ expect($("." + this.css).length).toBe(21);
+ results = [];
+ for (line = i = 5; i <= 25; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ it('scrolls to the first highlighted line on initial load', function() {
+ var spy;
+ spy = spyOn($, 'scrollTo');
+ new LineHighlighter('#L5-25');
+ return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything());
+ });
+ it('discards click events', function() {
+ var spy;
+ spy = spyOnEvent('a[data-line-number]', 'click');
+ clickLine(13);
+ return expect(spy).toHaveBeenPrevented();
+ });
+ return it('handles garbage input from the hash', function() {
+ var func;
+ func = function() {
+ return new LineHighlighter('#blob-content-holder');
+ };
+ return expect(func).not.toThrow();
+ });
+ });
+ describe('#clickHandler', function() {
+ it('discards the mousedown event', function() {
+ var spy;
+ spy = spyOnEvent('a[data-line-number]', 'mousedown');
+ clickLine(13);
+ return expect(spy).toHaveBeenPrevented();
+ });
+ it('handles clicking on a child icon element', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ $('#L13 i').mousedown().click();
+ expect(spy).toHaveBeenCalledWith(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ describe('without shiftKey', function() {
+ it('highlights one line when clicked', function() {
+ clickLine(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ it('unhighlights previously highlighted lines', function() {
+ clickLine(13);
+ clickLine(20);
+ expect($('#LC13')).not.toHaveClass(this.css);
+ return expect($('#LC20')).toHaveClass(this.css);
+ });
+ return it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ clickLine(13);
+ return expect(spy).toHaveBeenCalledWith(13);
+ });
+ });
+ return describe('with shiftKey', function() {
+ it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash').and.callThrough();
+ clickLine(13);
+ clickLine(20, {
+ shiftKey: true
+ });
+ expect(spy).toHaveBeenCalledWith(13);
+ return expect(spy).toHaveBeenCalledWith(13, 20);
+ });
+ describe('without existing highlight', function() {
+ it('highlights the clicked line', function() {
+ clickLine(13, {
+ shiftKey: true
+ });
+ expect($('#LC13')).toHaveClass(this.css);
+ return expect($("." + this.css).length).toBe(1);
+ });
+ return it('sets the hash', function() {
+ var spy;
+ spy = spyOn(this["class"], 'setHash');
+ clickLine(13, {
+ shiftKey: true
+ });
+ return expect(spy).toHaveBeenCalledWith(13);
+ });
+ });
+ describe('with existing single-line highlight', function() {
+ it('uses existing line as last line when target is lesser', function() {
+ var i, line, results;
+ clickLine(20);
+ clickLine(15, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 15; i <= 20; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ return it('uses existing line as first line when target is greater', function() {
+ var i, line, results;
+ clickLine(5);
+ clickLine(10, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 5; i <= 10; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ });
+ return describe('with existing multi-line highlight', function() {
+ beforeEach(function() {
+ clickLine(10, {
+ shiftKey: true
+ });
+ return clickLine(13, {
+ shiftKey: true
+ });
+ });
+ it('uses target as first line when it is less than existing first line', function() {
+ var i, line, results;
+ clickLine(5, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 5; i <= 10; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ return it('uses target as last line when it is greater than existing first line', function() {
+ var i, line, results;
+ clickLine(15, {
+ shiftKey: true
+ });
+ expect($("." + this.css).length).toBe(6);
+ results = [];
+ for (line = i = 10; i <= 15; line = ++i) {
+ results.push(expect($("#LC" + line)).toHaveClass(this.css));
+ }
+ return results;
+ });
+ });
+ });
+ });
+ describe('#hashToRange', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].hashToRange;
+ });
+ it('extracts a single line number from the hash', function() {
+ return expect(this.subject('#L5')).toEqual([5, null]);
+ });
+ it('extracts a range of line numbers from the hash', function() {
+ return expect(this.subject('#L5-15')).toEqual([5, 15]);
+ });
+ return it('returns [null, null] when the hash is not a line number', function() {
+ return expect(this.subject('#foo')).toEqual([null, null]);
+ });
+ });
+ describe('#highlightLine', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].highlightLine;
+ });
+ it('highlights the specified line', function() {
+ this.subject(13);
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ return it('accepts a String-based number', function() {
+ this.subject('13');
+ return expect($('#LC13')).toHaveClass(this.css);
+ });
+ });
+ return describe('#setHash', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].setHash;
+ });
+ it('sets the location hash for a single line', function() {
+ this.subject(5);
+ return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5');
+ });
+ return it('sets the location hash for a range', function() {
+ this.subject(5, 15);
+ return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee
deleted file mode 100644
index a073f21e7bc..00000000000
--- a/spec/javascripts/line_highlighter_spec.js.coffee
+++ /dev/null
@@ -1,158 +0,0 @@
-#= require line_highlighter
-
-describe 'LineHighlighter', ->
- fixture.preload('line_highlighter.html')
-
- clickLine = (number, eventData = {}) ->
- if $.isEmptyObject(eventData)
- $("#L#{number}").mousedown().click()
- else
- e = $.Event 'mousedown', eventData
- $("#L#{number}").trigger(e).click()
-
- beforeEach ->
- fixture.load('line_highlighter.html')
- @class = new LineHighlighter()
- @css = @class.highlightClass
- @spies = {
- __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake ->
- }
-
- describe 'behavior', ->
- it 'highlights one line given in the URL hash', ->
- new LineHighlighter('#L13')
- expect($('#LC13')).toHaveClass(@css)
-
- it 'highlights a range of lines given in the URL hash', ->
- new LineHighlighter('#L5-25')
- expect($(".#{@css}").length).toBe(21)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25]
-
- it 'scrolls to the first highlighted line on initial load', ->
- spy = spyOn($, 'scrollTo')
- new LineHighlighter('#L5-25')
- expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything())
-
- it 'discards click events', ->
- spy = spyOnEvent('a[data-line-number]', 'click')
- clickLine(13)
- expect(spy).toHaveBeenPrevented()
-
- it 'handles garbage input from the hash', ->
- func = -> new LineHighlighter('#blob-content-holder')
- expect(func).not.toThrow()
-
- describe '#clickHandler', ->
- it 'discards the mousedown event', ->
- spy = spyOnEvent('a[data-line-number]', 'mousedown')
- clickLine(13)
- expect(spy).toHaveBeenPrevented()
-
- it 'handles clicking on a child icon element', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
-
- $('#L13 i').mousedown().click()
-
- expect(spy).toHaveBeenCalledWith(13)
- expect($('#LC13')).toHaveClass(@css)
-
- describe 'without shiftKey', ->
- it 'highlights one line when clicked', ->
- clickLine(13)
- expect($('#LC13')).toHaveClass(@css)
-
- it 'unhighlights previously highlighted lines', ->
- clickLine(13)
- clickLine(20)
-
- expect($('#LC13')).not.toHaveClass(@css)
- expect($('#LC20')).toHaveClass(@css)
-
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
- clickLine(13)
- expect(spy).toHaveBeenCalledWith(13)
-
- describe 'with shiftKey', ->
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash').and.callThrough()
- clickLine(13)
- clickLine(20, shiftKey: true)
- expect(spy).toHaveBeenCalledWith(13)
- expect(spy).toHaveBeenCalledWith(13, 20)
-
- describe 'without existing highlight', ->
- it 'highlights the clicked line', ->
- clickLine(13, shiftKey: true)
- expect($('#LC13')).toHaveClass(@css)
- expect($(".#{@css}").length).toBe(1)
-
- it 'sets the hash', ->
- spy = spyOn(@class, 'setHash')
- clickLine(13, shiftKey: true)
- expect(spy).toHaveBeenCalledWith(13)
-
- describe 'with existing single-line highlight', ->
- it 'uses existing line as last line when target is lesser', ->
- clickLine(20)
- clickLine(15, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20]
-
- it 'uses existing line as first line when target is greater', ->
- clickLine(5)
- clickLine(10, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
-
- describe 'with existing multi-line highlight', ->
- beforeEach ->
- clickLine(10, shiftKey: true)
- clickLine(13, shiftKey: true)
-
- it 'uses target as first line when it is less than existing first line', ->
- clickLine(5, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10]
-
- it 'uses target as last line when it is greater than existing first line', ->
- clickLine(15, shiftKey: true)
- expect($(".#{@css}").length).toBe(6)
- expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15]
-
- describe '#hashToRange', ->
- beforeEach ->
- @subject = @class.hashToRange
-
- it 'extracts a single line number from the hash', ->
- expect(@subject('#L5')).toEqual([5, null])
-
- it 'extracts a range of line numbers from the hash', ->
- expect(@subject('#L5-15')).toEqual([5, 15])
-
- it 'returns [null, null] when the hash is not a line number', ->
- expect(@subject('#foo')).toEqual([null, null])
-
- describe '#highlightLine', ->
- beforeEach ->
- @subject = @class.highlightLine
-
- it 'highlights the specified line', ->
- @subject(13)
- expect($('#LC13')).toHaveClass(@css)
-
- it 'accepts a String-based number', ->
- @subject('13')
- expect($('#LC13')).toHaveClass(@css)
-
- describe '#setHash', ->
- beforeEach ->
- @subject = @class.setHash
-
- it 'sets the location hash for a single line', ->
- @subject(5)
- expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5')
-
- it 'sets the location hash for a range', ->
- @subject(5, 15)
- expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15')
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
new file mode 100644
index 00000000000..61830d267a9
--- /dev/null
+++ b/spec/javascripts/merge_request_spec.js
@@ -0,0 +1,28 @@
+
+/*= require merge_request */
+
+(function() {
+ describe('MergeRequest', function() {
+ return describe('task lists', function() {
+ fixture.preload('merge_requests_show.html');
+ beforeEach(function() {
+ fixture.load('merge_requests_show.html');
+ return this.merge = new MergeRequest();
+ });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return 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('/foo');
+ return expect(req.data.merge_request.description).not.toBe(null);
+ });
+ return $('.js-task-list-field').trigger('tasklist:changed');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
deleted file mode 100644
index 3cb67d51c85..00000000000
--- a/spec/javascripts/merge_request_spec.js.coffee
+++ /dev/null
@@ -1,23 +0,0 @@
-#= require merge_request
-
-describe 'MergeRequest', ->
- describe 'task lists', ->
- fixture.preload('merge_requests_show.html')
-
- beforeEach ->
- fixture.load('merge_requests_show.html')
- @merge = new MergeRequest()
-
- it 'modifies the Markdown field', ->
- spyOn(jQuery, 'ajax').and.stub()
-
- $('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', ->
- spyOn(jQuery, 'ajax').and.callFake (req) ->
- expect(req.type).toBe('PATCH')
- expect(req.url).toBe('/foo')
- expect(req.data.merge_request.description).not.toBe(null)
-
- $('.js-task-list-field').trigger('tasklist:changed')
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
new file mode 100644
index 00000000000..395032a7416
--- /dev/null
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -0,0 +1,106 @@
+
+/*= require merge_request_tabs */
+
+(function() {
+ describe('MergeRequestTabs', function() {
+ var stubLocation;
+ stubLocation = function(stubs) {
+ var defaults;
+ defaults = {
+ pathname: '',
+ search: '',
+ hash: ''
+ };
+ return $.extend(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() {})
+ };
+ });
+ describe('#activateTab', function() {
+ beforeEach(function() {
+ fixture.load('merge_request_tabs.html');
+ return this.subject = this["class"].activateTab;
+ });
+ it('shows the first tab when action is show', function() {
+ this.subject('show');
+ return expect($('#notes')).toHaveClass('active');
+ });
+ it('shows the notes tab when action is notes', function() {
+ this.subject('notes');
+ return expect($('#notes')).toHaveClass('active');
+ });
+ it('shows the commits tab when action is commits', function() {
+ this.subject('commits');
+ return expect($('#commits')).toHaveClass('active');
+ });
+ return it('shows the diffs tab when action is diffs', function() {
+ this.subject('diffs');
+ return expect($('#diffs')).toHaveClass('active');
+ });
+ });
+ return describe('#setCurrentAction', function() {
+ beforeEach(function() {
+ return this.subject = this["class"].setCurrentAction;
+ });
+ it('changes from commits', function() {
+ this["class"]._location = stubLocation({
+ 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');
+ });
+ it('changes from diffs', function() {
+ this["class"]._location = stubLocation({
+ 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');
+ });
+ it('changes from diffs.html', function() {
+ this["class"]._location = stubLocation({
+ 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');
+ });
+ it('changes from notes', function() {
+ this["class"]._location = stubLocation({
+ 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');
+ });
+ it('includes search parameters and hash string', function() {
+ this["class"]._location = stubLocation({
+ 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');
+ });
+ it('replaces the current history state', function() {
+ var new_state;
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1'
+ });
+ new_state = this.subject('commits');
+ return expect(this.spies.history).toHaveBeenCalledWith({
+ turbolinks: true,
+ url: new_state
+ }, document.title, new_state);
+ });
+ return it('treats "show" like "notes"', function() {
+ this["class"]._location = stubLocation({
+ pathname: '/foo/bar/merge_requests/1/commits'
+ });
+ return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee
deleted file mode 100644
index a0cfba455ea..00000000000
--- a/spec/javascripts/merge_request_tabs_spec.js.coffee
+++ /dev/null
@@ -1,88 +0,0 @@
-#= require merge_request_tabs
-
-describe 'MergeRequestTabs', ->
- stubLocation = (stubs) ->
- defaults = {pathname: '', search: '', hash: ''}
- $.extend(defaults, stubs)
-
- fixture.preload('merge_request_tabs.html')
-
- beforeEach ->
- @class = new MergeRequestTabs()
- @spies = {
- ajax: spyOn($, 'ajax').and.callFake ->
- history: spyOn(history, 'replaceState').and.callFake ->
- }
-
- describe '#activateTab', ->
- beforeEach ->
- fixture.load('merge_request_tabs.html')
- @subject = @class.activateTab
-
- it 'shows the first tab when action is show', ->
- @subject('show')
- expect($('#notes')).toHaveClass('active')
-
- it 'shows the notes tab when action is notes', ->
- @subject('notes')
- expect($('#notes')).toHaveClass('active')
-
- it 'shows the commits tab when action is commits', ->
- @subject('commits')
- expect($('#commits')).toHaveClass('active')
-
- it 'shows the diffs tab when action is diffs', ->
- @subject('diffs')
- expect($('#diffs')).toHaveClass('active')
-
- describe '#setCurrentAction', ->
- beforeEach ->
- @subject = @class.setCurrentAction
-
- it 'changes from commits', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
-
- it 'changes from diffs', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'changes from diffs.html', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html')
-
- expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'changes from notes', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
-
- expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
- expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
-
- it 'includes search parameters and hash string', ->
- @class._location = stubLocation({
- pathname: '/foo/bar/merge_requests/1/diffs'
- search: '?view=parallel'
- hash: '#L15-35'
- })
-
- expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35')
-
- it 'replaces the current history state', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
- new_state = @subject('commits')
-
- expect(@spies.history).toHaveBeenCalledWith(
- {turbolinks: true, url: new_state},
- document.title,
- new_state
- )
-
- it 'treats "show" like "notes"', ->
- @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
-
- expect(@subject('show')).toBe('/foo/bar/merge_requests/1')
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
new file mode 100644
index 00000000000..17b32914ec3
--- /dev/null
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -0,0 +1,74 @@
+
+/*= require merge_request_widget */
+
+(function() {
+ describe('MergeRequestWidget', function() {
+ beforeEach(function() {
+ window.notifyPermissions = function() {};
+ window.notify = function() {};
+ this.opts = {
+ ci_status_url: "http://sampledomain.local/ci/getstatus",
+ ci_status: "",
+ ci_message: {
+ normal: "Build {{status}} for \"{{title}}\"",
+ preparing: "{{status}} build for \"{{title}}\""
+ },
+ ci_title: {
+ preparing: "{{status}} build",
+ normal: "Build {{status}}"
+ },
+ gitlab_icon: "gitlab_logo.png",
+ builds_path: "http://sampledomain.local/sampleBuildsPath"
+ };
+ this["class"] = new MergeRequestWidget(this.opts);
+ return this.ciStatusData = {
+ "title": "Sample MR title",
+ "sha": "12a34bc5",
+ "status": "success",
+ "coverage": 98
+ };
+ });
+ return describe('getCIStatus', function() {
+ beforeEach(function() {
+ return spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
+ return function(req, cb) {
+ return cb(_this.ciStatusData);
+ };
+ })(this));
+ });
+ it('should call showCIStatus even if a notification should not be displayed', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
+ });
+ it('should call showCIStatus when a notification should be displayed', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ this["class"].getCIStatus(true);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
+ });
+ it('should call showCICoverage when the coverage rate is set', function() {
+ var spy;
+ spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
+ });
+ it('should not call showCICoverage when the coverage rate is not set', function() {
+ var spy;
+ this.ciStatusData.coverage = null;
+ spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ this["class"].getCIStatus(false);
+ return expect(spy).not.toHaveBeenCalled();
+ });
+ return it('should not display a notification on the first check after the widget has been created', function() {
+ var spy;
+ spy = spyOn(window, 'notify');
+ this["class"] = new MergeRequestWidget(this.opts);
+ this["class"].getCIStatus(true);
+ return expect(spy).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee
deleted file mode 100644
index 92b7eeb1116..00000000000
--- a/spec/javascripts/merge_request_widget_spec.js.coffee
+++ /dev/null
@@ -1,55 +0,0 @@
-#= require merge_request_widget
-
-describe 'MergeRequestWidget', ->
-
- beforeEach ->
- window.notifyPermissions = () ->
- window.notify = () ->
- @opts = {
- ci_status_url:"http://sampledomain.local/ci/getstatus",
- ci_status:"",
- ci_message: {
- normal: "Build {{status}} for \"{{title}}\"",
- preparing: "{{status}} build for \"{{title}}\""
- },
- ci_title: {
- preparing: "{{status}} build",
- normal: "Build {{status}}"
- },
- gitlab_icon:"gitlab_logo.png",
- builds_path:"http://sampledomain.local/sampleBuildsPath"
- }
- @class = new MergeRequestWidget(@opts)
- @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98}
-
- describe 'getCIStatus', ->
- beforeEach ->
- spyOn(jQuery, 'getJSON').and.callFake (req, cb) =>
- cb(@ciStatusData)
-
- it 'should call showCIStatus even if a notification should not be displayed', ->
- spy = spyOn(@class, 'showCIStatus').and.stub()
- @class.getCIStatus(false)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.status)
-
- it 'should call showCIStatus when a notification should be displayed', ->
- spy = spyOn(@class, 'showCIStatus').and.stub()
- @class.getCIStatus(true)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.status)
-
- it 'should call showCICoverage when the coverage rate is set', ->
- spy = spyOn(@class, 'showCICoverage').and.stub()
- @class.getCIStatus(false)
- expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage)
-
- it 'should not call showCICoverage when the coverage rate is not set', ->
- @ciStatusData.coverage = null
- spy = spyOn(@class, 'showCICoverage').and.stub()
- @class.getCIStatus(false)
- expect(spy).not.toHaveBeenCalled()
-
- it 'should not display a notification on the first check after the widget has been created', ->
- spy = spyOn(window, 'notify')
- @class = new MergeRequestWidget(@opts)
- @class.getCIStatus(true)
- expect(spy).not.toHaveBeenCalled()
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
new file mode 100644
index 00000000000..25d3f5b6c04
--- /dev/null
+++ b/spec/javascripts/new_branch_spec.js
@@ -0,0 +1,170 @@
+
+/*= require jquery-ui/autocomplete */
+
+
+/*= require new_branch_form */
+
+(function() {
+ describe('Branch', function() {
+ return describe('create a new branch', function() {
+ var expectToHaveError, fillNameWith;
+ fixture.preload('new_branch.html');
+ fillNameWith = function(value) {
+ return $('.js-branch-name').val(value).trigger('blur');
+ };
+ expectToHaveError = function(error) {
+ return expect($('.js-branch-name-error span').text()).toEqual(error);
+ };
+ beforeEach(function() {
+ fixture.load('new_branch.html');
+ $('form').on('submit', function(e) {
+ return e.preventDefault();
+ });
+ return this.form = new NewBranchForm($('.js-create-branch-form'), []);
+ });
+ it("can't start with a dot", function() {
+ fillNameWith('.foo');
+ return expectToHaveError("can't start with '.'");
+ });
+ it("can't start with a slash", function() {
+ fillNameWith('/foo');
+ return expectToHaveError("can't start with '/'");
+ });
+ it("can't have two consecutive dots", function() {
+ fillNameWith('foo..bar');
+ return expectToHaveError("can't contain '..'");
+ });
+ it("can't have spaces anywhere", function() {
+ fillNameWith(' foo');
+ expectToHaveError("can't contain spaces");
+ fillNameWith('foo bar');
+ expectToHaveError("can't contain spaces");
+ fillNameWith('foo ');
+ return expectToHaveError("can't contain spaces");
+ });
+ it("can't have ~ anywhere", function() {
+ fillNameWith('~foo');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~bar');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~');
+ return expectToHaveError("can't contain '~'");
+ });
+ it("can't have tilde anwhere", function() {
+ fillNameWith('~foo');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~bar');
+ expectToHaveError("can't contain '~'");
+ fillNameWith('foo~');
+ return expectToHaveError("can't contain '~'");
+ });
+ it("can't have caret anywhere", function() {
+ fillNameWith('^foo');
+ expectToHaveError("can't contain '^'");
+ fillNameWith('foo^bar');
+ expectToHaveError("can't contain '^'");
+ fillNameWith('foo^');
+ return expectToHaveError("can't contain '^'");
+ });
+ it("can't have : anywhere", function() {
+ fillNameWith(':foo');
+ expectToHaveError("can't contain ':'");
+ fillNameWith('foo:bar');
+ expectToHaveError("can't contain ':'");
+ fillNameWith(':foo');
+ return expectToHaveError("can't contain ':'");
+ });
+ it("can't have question mark anywhere", function() {
+ fillNameWith('?foo');
+ expectToHaveError("can't contain '?'");
+ fillNameWith('foo?bar');
+ expectToHaveError("can't contain '?'");
+ fillNameWith('foo?');
+ return expectToHaveError("can't contain '?'");
+ });
+ it("can't have asterisk anywhere", function() {
+ fillNameWith('*foo');
+ expectToHaveError("can't contain '*'");
+ fillNameWith('foo*bar');
+ expectToHaveError("can't contain '*'");
+ fillNameWith('foo*');
+ return expectToHaveError("can't contain '*'");
+ });
+ it("can't have open bracket anywhere", function() {
+ fillNameWith('[foo');
+ expectToHaveError("can't contain '['");
+ fillNameWith('foo[bar');
+ expectToHaveError("can't contain '['");
+ fillNameWith('foo[');
+ return expectToHaveError("can't contain '['");
+ });
+ it("can't have a backslash anywhere", function() {
+ fillNameWith('\\foo');
+ expectToHaveError("can't contain '\\'");
+ fillNameWith('foo\\bar');
+ expectToHaveError("can't contain '\\'");
+ fillNameWith('foo\\');
+ return expectToHaveError("can't contain '\\'");
+ });
+ it("can't contain a sequence @{ anywhere", function() {
+ fillNameWith('@{foo');
+ expectToHaveError("can't contain '@{'");
+ fillNameWith('foo@{bar');
+ expectToHaveError("can't contain '@{'");
+ fillNameWith('foo@{');
+ return expectToHaveError("can't contain '@{'");
+ });
+ it("can't have consecutive slashes", function() {
+ fillNameWith('foo//bar');
+ return expectToHaveError("can't contain consecutive slashes");
+ });
+ it("can't end with a slash", function() {
+ fillNameWith('foo/');
+ return expectToHaveError("can't end in '/'");
+ });
+ it("can't end with a dot", function() {
+ fillNameWith('foo.');
+ return expectToHaveError("can't end in '.'");
+ });
+ it("can't end with .lock", function() {
+ fillNameWith('foo.lock');
+ return expectToHaveError("can't end in '.lock'");
+ });
+ it("can't be the single character @", function() {
+ fillNameWith('@');
+ return expectToHaveError("can't be '@'");
+ });
+ it("concatenates all error messages", function() {
+ fillNameWith('/foo bar?~.');
+ return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'");
+ });
+ it("doesn't duplicate error messages", function() {
+ fillNameWith('?foo?bar?zoo?');
+ return expectToHaveError("can't contain '?'");
+ });
+ it("removes the error message when is a valid name", function() {
+ fillNameWith('foo?bar');
+ expect($('.js-branch-name-error span').length).toEqual(1);
+ fillNameWith('foobar');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have dashes anywhere", function() {
+ fillNameWith('-foo-bar-zoo-');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have underscores anywhere", function() {
+ fillNameWith('_foo_bar_zoo_');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ it("can have numbers anywhere", function() {
+ fillNameWith('1foo2bar3zoo4');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ return it("can be only letters", function() {
+ fillNameWith('foo');
+ return expect($('.js-branch-name-error span').length).toEqual(0);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee
deleted file mode 100644
index ce773793817..00000000000
--- a/spec/javascripts/new_branch_spec.js.coffee
+++ /dev/null
@@ -1,160 +0,0 @@
-#= require jquery-ui/autocomplete
-#= require new_branch_form
-
-describe 'Branch', ->
- describe 'create a new branch', ->
- fixture.preload('new_branch.html')
-
- fillNameWith = (value) ->
- $('.js-branch-name').val(value).trigger('blur')
-
- expectToHaveError = (error) ->
- expect($('.js-branch-name-error span').text()).toEqual(error)
-
- beforeEach ->
- fixture.load('new_branch.html')
- $('form').on 'submit', (e) -> e.preventDefault()
-
- @form = new NewBranchForm($('.js-create-branch-form'), [])
-
- it "can't start with a dot", ->
- fillNameWith '.foo'
- expectToHaveError "can't start with '.'"
-
- it "can't start with a slash", ->
- fillNameWith '/foo'
- expectToHaveError "can't start with '/'"
-
- it "can't have two consecutive dots", ->
- fillNameWith 'foo..bar'
- expectToHaveError "can't contain '..'"
-
- it "can't have spaces anywhere", ->
- fillNameWith ' foo'
- expectToHaveError "can't contain spaces"
- fillNameWith 'foo bar'
- expectToHaveError "can't contain spaces"
- fillNameWith 'foo '
- expectToHaveError "can't contain spaces"
-
- it "can't have ~ anywhere", ->
- fillNameWith '~foo'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~bar'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~'
- expectToHaveError "can't contain '~'"
-
- it "can't have tilde anwhere", ->
- fillNameWith '~foo'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~bar'
- expectToHaveError "can't contain '~'"
- fillNameWith 'foo~'
- expectToHaveError "can't contain '~'"
-
- it "can't have caret anywhere", ->
- fillNameWith '^foo'
- expectToHaveError "can't contain '^'"
- fillNameWith 'foo^bar'
- expectToHaveError "can't contain '^'"
- fillNameWith 'foo^'
- expectToHaveError "can't contain '^'"
-
- it "can't have : anywhere", ->
- fillNameWith ':foo'
- expectToHaveError "can't contain ':'"
- fillNameWith 'foo:bar'
- expectToHaveError "can't contain ':'"
- fillNameWith ':foo'
- expectToHaveError "can't contain ':'"
-
- it "can't have question mark anywhere", ->
- fillNameWith '?foo'
- expectToHaveError "can't contain '?'"
- fillNameWith 'foo?bar'
- expectToHaveError "can't contain '?'"
- fillNameWith 'foo?'
- expectToHaveError "can't contain '?'"
-
- it "can't have asterisk anywhere", ->
- fillNameWith '*foo'
- expectToHaveError "can't contain '*'"
- fillNameWith 'foo*bar'
- expectToHaveError "can't contain '*'"
- fillNameWith 'foo*'
- expectToHaveError "can't contain '*'"
-
- it "can't have open bracket anywhere", ->
- fillNameWith '[foo'
- expectToHaveError "can't contain '['"
- fillNameWith 'foo[bar'
- expectToHaveError "can't contain '['"
- fillNameWith 'foo['
- expectToHaveError "can't contain '['"
-
- it "can't have a backslash anywhere", ->
- fillNameWith '\\foo'
- expectToHaveError "can't contain '\\'"
- fillNameWith 'foo\\bar'
- expectToHaveError "can't contain '\\'"
- fillNameWith 'foo\\'
- expectToHaveError "can't contain '\\'"
-
- it "can't contain a sequence @{ anywhere", ->
- fillNameWith '@{foo'
- expectToHaveError "can't contain '@{'"
- fillNameWith 'foo@{bar'
- expectToHaveError "can't contain '@{'"
- fillNameWith 'foo@{'
- expectToHaveError "can't contain '@{'"
-
- it "can't have consecutive slashes", ->
- fillNameWith 'foo//bar'
- expectToHaveError "can't contain consecutive slashes"
-
- it "can't end with a slash", ->
- fillNameWith 'foo/'
- expectToHaveError "can't end in '/'"
-
- it "can't end with a dot", ->
- fillNameWith 'foo.'
- expectToHaveError "can't end in '.'"
-
- it "can't end with .lock", ->
- fillNameWith 'foo.lock'
- expectToHaveError "can't end in '.lock'"
-
- it "can't be the single character @", ->
- fillNameWith '@'
- expectToHaveError "can't be '@'"
-
- it "concatenates all error messages", ->
- fillNameWith '/foo bar?~.'
- expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'"
-
- it "doesn't duplicate error messages", ->
- fillNameWith '?foo?bar?zoo?'
- expectToHaveError "can't contain '?'"
-
- it "removes the error message when is a valid name", ->
- fillNameWith 'foo?bar'
- expect($('.js-branch-name-error span').length).toEqual(1)
- fillNameWith 'foobar'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have dashes anywhere", ->
- fillNameWith '-foo-bar-zoo-'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have underscores anywhere", ->
- fillNameWith '_foo_bar_zoo_'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can have numbers anywhere", ->
- fillNameWith '1foo2bar3zoo4'
- expect($('.js-branch-name-error span').length).toEqual(0)
-
- it "can be only letters", ->
- fillNameWith 'foo'
- expect($('.js-branch-name-error span').length).toEqual(0)
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
new file mode 100644
index 00000000000..14dc6bfdfde
--- /dev/null
+++ b/spec/javascripts/notes_spec.js
@@ -0,0 +1,41 @@
+
+/*= require notes */
+
+
+/*= require gl_form */
+
+(function() {
+ window.gon || (window.gon = {});
+
+ window.disableButtonIfEmptyField = function() {
+ return null;
+ };
+
+ describe('Notes', function() {
+ return describe('task lists', function() {
+ fixture.preload('issue_note.html');
+ beforeEach(function() {
+ fixture.load('issue_note.html');
+ $('form').on('submit', function(e) {
+ return e.preventDefault();
+ });
+ return this.notes = new Notes();
+ });
+ it('modifies the Markdown field', function() {
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
+ return it('submits the form on tasklist:changed', function() {
+ var submitted;
+ submitted = false;
+ $('form').on('submit', function(e) {
+ submitted = true;
+ return e.preventDefault();
+ });
+ $('.js-task-list-field').trigger('tasklist:changed');
+ return expect(submitted).toBe(true);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
deleted file mode 100644
index 3a3c8d63e82..00000000000
--- a/spec/javascripts/notes_spec.js.coffee
+++ /dev/null
@@ -1,26 +0,0 @@
-#= require notes
-#= require gl_form
-
-window.gon or= {}
-window.disableButtonIfEmptyField = -> null
-
-describe 'Notes', ->
- describe 'task lists', ->
- fixture.preload('issue_note.html')
-
- beforeEach ->
- fixture.load('issue_note.html')
- $('form').on 'submit', (e) -> e.preventDefault()
-
- @notes = new Notes()
-
- it 'modifies the Markdown field', ->
- $('input[type=checkbox]').attr('checked', true).trigger('change')
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item')
-
- it 'submits the form on tasklist:changed', ->
- submitted = false
- $('form').on 'submit', (e) -> submitted = true; e.preventDefault()
-
- $('.js-task-list-field').trigger('tasklist:changed')
- expect(submitted).toBe(true)
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
new file mode 100644
index 00000000000..ffe49828492
--- /dev/null
+++ b/spec/javascripts/project_title_spec.js
@@ -0,0 +1,60 @@
+
+/*= require bootstrap */
+
+
+/*= require select2 */
+
+
+/*= require lib/utils/type_utility */
+
+
+/*= require gl_dropdown */
+
+
+/*= require api */
+
+
+/*= require project_select */
+
+
+/*= require project */
+
+(function() {
+ window.gon || (window.gon = {});
+
+ window.gon.api_version = 'v3';
+
+ describe('Project Title', function() {
+ fixture.preload('project_title.html');
+ fixture.preload('projects.json');
+ beforeEach(function() {
+ fixture.load('project_title.html');
+ return this.project = new Project();
+ });
+ return describe('project list', function() {
+ 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();
+ });
+ };
+ })(this));
+ it('to show on toggle click', (function(_this) {
+ return function() {
+ $('.js-projects-dropdown-toggle').click();
+ return expect($('.header-content').hasClass('open')).toBe(true);
+ };
+ })(this));
+ return it('hide dropdown', function() {
+ $(".dropdown-menu-close-icon").click();
+ return expect($('.header-content').hasClass('open')).toBe(false);
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee
deleted file mode 100644
index f0d26fb5446..00000000000
--- a/spec/javascripts/project_title_spec.js.coffee
+++ /dev/null
@@ -1,37 +0,0 @@
-#= require bootstrap
-#= require select2
-#= require lib/utils/type_utility
-#= require gl_dropdown
-#= require api
-#= require project_select
-#= require project
-
-window.gon or= {}
-window.gon.api_version = 'v3'
-
-describe 'Project Title', ->
- fixture.preload('project_title.html')
- fixture.preload('projects.json')
-
- beforeEach ->
- fixture.load('project_title.html')
- @project = new Project()
-
- describe 'project list', ->
- beforeEach =>
- @projects_data = fixture.load('projects.json')[0]
-
- spyOn(jQuery, 'ajax').and.callFake (req) =>
- expect(req.url).toBe('/api/v3/projects.json')
- d = $.Deferred()
- d.resolve @projects_data
- d.promise()
-
- it 'to show on toggle click', =>
- $('.js-projects-dropdown-toggle').click()
- expect($('.header-content').hasClass('open')).toBe(true)
-
- it 'hide dropdown', ->
- $(".dropdown-menu-close-icon").click()
-
- expect($('.header-content').hasClass('open')).toBe(false)
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
new file mode 100644
index 00000000000..38b3b2653ec
--- /dev/null
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -0,0 +1,70 @@
+
+/*= require right_sidebar */
+
+
+/*= require jquery */
+
+
+/*= require jquery.cookie */
+
+(function() {
+ var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
+
+ this.sidebar = null;
+
+ $aside = null;
+
+ $toggle = null;
+
+ $icon = null;
+
+ $page = null;
+
+ $labelsIcon = null;
+
+ assertSidebarState = function(state) {
+ var shouldBeCollapsed, shouldBeExpanded;
+ shouldBeExpanded = state === 'expanded';
+ shouldBeCollapsed = state === 'collapsed';
+ expect($aside.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded);
+ expect($page.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded);
+ expect($icon.hasClass('fa-angle-double-right')).toBe(shouldBeExpanded);
+ expect($aside.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed);
+ expect($page.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed);
+ return expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed);
+ };
+
+ describe('RightSidebar', function() {
+ fixture.preload('right_sidebar.html');
+ beforeEach(function() {
+ fixture.load('right_sidebar.html');
+ this.sidebar = new Sidebar;
+ $aside = $('.right-sidebar');
+ $page = $('.page-with-sidebar');
+ $icon = $aside.find('i');
+ $toggle = $aside.find('.js-sidebar-toggle');
+ return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
+ });
+ it('should expand the sidebar when arrow is clicked', function() {
+ $toggle.click();
+ return assertSidebarState('expanded');
+ });
+ it('should collapse the sidebar when arrow is clicked', function() {
+ $toggle.click();
+ assertSidebarState('expanded');
+ $toggle.click();
+ return assertSidebarState('collapsed');
+ });
+ it('should float over the page and when sidebar icons clicked', function() {
+ $labelsIcon.click();
+ return assertSidebarState('expanded');
+ });
+ return it('should collapse when the icon arrow clicked while it is floating on page', function() {
+ $labelsIcon.click();
+ assertSidebarState('expanded');
+ $toggle.click();
+ return assertSidebarState('collapsed');
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee
deleted file mode 100644
index 2075cacdb67..00000000000
--- a/spec/javascripts/right_sidebar_spec.js.coffee
+++ /dev/null
@@ -1,69 +0,0 @@
-#= require right_sidebar
-#= require jquery
-#= require jquery.cookie
-
-@sidebar = null
-$aside = null
-$toggle = null
-$icon = null
-$page = null
-$labelsIcon = null
-
-
-assertSidebarState = (state) ->
-
- shouldBeExpanded = state is 'expanded'
- shouldBeCollapsed = state is 'collapsed'
-
- expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
- expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded
- expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded
-
- expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
- expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed
- expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed
-
-
-describe 'RightSidebar', ->
-
- fixture.preload 'right_sidebar.html'
-
- beforeEach ->
- fixture.load 'right_sidebar.html'
-
- @sidebar = new Sidebar
- $aside = $ '.right-sidebar'
- $page = $ '.page-with-sidebar'
- $icon = $aside.find 'i'
- $toggle = $aside.find '.js-sidebar-toggle'
- $labelsIcon = $aside.find '.sidebar-collapsed-icon'
-
-
- it 'should expand the sidebar when arrow is clicked', ->
-
- $toggle.click()
- assertSidebarState 'expanded'
-
-
- it 'should collapse the sidebar when arrow is clicked', ->
-
- $toggle.click()
- assertSidebarState 'expanded'
-
- $toggle.click()
- assertSidebarState 'collapsed'
-
-
- it 'should float over the page and when sidebar icons clicked', ->
-
- $labelsIcon.click()
- assertSidebarState 'expanded'
-
-
- it 'should collapse when the icon arrow clicked while it is floating on page', ->
-
- $labelsIcon.click()
- assertSidebarState 'expanded'
-
- $toggle.click()
- assertSidebarState 'collapsed'
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
new file mode 100644
index 00000000000..324f5152780
--- /dev/null
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -0,0 +1,159 @@
+
+/*= require gl_dropdown */
+
+
+/*= require search_autocomplete */
+
+
+/*= require jquery */
+
+
+/*= require lib/utils/common_utils */
+
+
+/*= require lib/utils/type_utility */
+
+
+/*= require fuzzaldrin-plus */
+
+(function() {
+ var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
+
+ widget = null;
+
+ userId = 1;
+
+ window.gon || (window.gon = {});
+
+ window.gon.current_user_id = userId;
+
+ dashboardIssuesPath = '/dashboard/issues';
+
+ dashboardMRsPath = '/dashboard/merge_requests';
+
+ projectIssuesPath = '/gitlab-org/gitlab-ce/issues';
+
+ projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests';
+
+ groupIssuesPath = '/groups/gitlab-org/issues';
+
+ groupMRsPath = '/groups/gitlab-org/merge_requests';
+
+ projectName = 'GitLab Community Edition';
+
+ groupName = 'Gitlab Org';
+
+ addBodyAttributes = function(section) {
+ var $body;
+ if (section == null) {
+ section = 'dashboard';
+ }
+ $body = $('body');
+ $body.removeAttr('data-page');
+ $body.removeAttr('data-project');
+ $body.removeAttr('data-group');
+ switch (section) {
+ case 'dashboard':
+ return $body.data('page', 'root:index');
+ case 'group':
+ $body.data('page', 'groups:show');
+ return $body.data('group', 'gitlab-org');
+ case 'project':
+ $body.data('page', 'projects:show');
+ return $body.data('project', 'gitlab-ce');
+ }
+ };
+
+ mockDashboardOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.dashboardOptions = {
+ issuesPath: dashboardIssuesPath,
+ mrPath: dashboardMRsPath
+ };
+ };
+
+ mockProjectOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.projectOptions = {
+ 'gitlab-ce': {
+ issuesPath: projectIssuesPath,
+ mrPath: projectMRsPath,
+ projectName: projectName
+ }
+ };
+ };
+
+ mockGroupOptions = function() {
+ window.gl || (window.gl = {});
+ return window.gl.groupOptions = {
+ 'gitlab-org': {
+ issuesPath: groupIssuesPath,
+ mrPath: groupMRsPath,
+ projectName: groupName
+ }
+ };
+ };
+
+ assertLinks = function(list, issuesPath, mrsPath) {
+ var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink;
+ issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId;
+ issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId;
+ mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId;
+ mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId;
+ a1 = "a[href='" + issuesAssignedToMeLink + "']";
+ a2 = "a[href='" + issuesIHaveCreatedLink + "']";
+ a3 = "a[href='" + mrsAssignedToMeLink + "']";
+ a4 = "a[href='" + mrsIHaveCreatedLink + "']";
+ expect(list.find(a1).length).toBe(1);
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
+ expect(list.find(a2).length).toBe(1);
+ expect(list.find(a2).text()).toBe("Issues I've created");
+ expect(list.find(a3).length).toBe(1);
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
+ expect(list.find(a4).length).toBe(1);
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
+ };
+
+ describe('Search autocomplete dropdown', function() {
+ fixture.preload('search_autocomplete.html');
+ beforeEach(function() {
+ fixture.load('search_autocomplete.html');
+ return widget = new SearchAutocomplete;
+ });
+ it('should show Dashboard specific dropdown menu', function() {
+ var list;
+ addBodyAttributes();
+ mockDashboardOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, dashboardIssuesPath, dashboardMRsPath);
+ });
+ it('should show Group specific dropdown menu', function() {
+ var list;
+ addBodyAttributes('group');
+ mockGroupOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, groupIssuesPath, groupMRsPath);
+ });
+ it('should show Project specific dropdown menu', function() {
+ var list;
+ addBodyAttributes('project');
+ mockProjectOptions();
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ return assertLinks(list, projectIssuesPath, projectMRsPath);
+ });
+ return it('should not show category related menu if there is text in the input', function() {
+ var link, list;
+ addBodyAttributes('project');
+ mockProjectOptions();
+ widget.searchInput.val('help');
+ widget.searchInput.focus();
+ list = widget.wrap.find('.dropdown-menu').find('ul');
+ link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
+ return expect(list.find(link).length).toBe(0);
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee
deleted file mode 100644
index 1c1faca3333..00000000000
--- a/spec/javascripts/search_autocomplete_spec.js.coffee
+++ /dev/null
@@ -1,149 +0,0 @@
-#= require gl_dropdown
-#= require search_autocomplete
-#= require jquery
-#= require lib/utils/common_utils
-#= require lib/utils/type_utility
-#= require fuzzaldrin-plus
-
-
-widget = null
-userId = 1
-window.gon or= {}
-window.gon.current_user_id = userId
-
-dashboardIssuesPath = '/dashboard/issues'
-dashboardMRsPath = '/dashboard/merge_requests'
-projectIssuesPath = '/gitlab-org/gitlab-ce/issues'
-projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'
-groupIssuesPath = '/groups/gitlab-org/issues'
-groupMRsPath = '/groups/gitlab-org/merge_requests'
-projectName = 'GitLab Community Edition'
-groupName = 'Gitlab Org'
-
-
-# Add required attributes to body before starting the test.
-# section would be dashboard|group|project
-addBodyAttributes = (section = 'dashboard') ->
-
- $body = $ 'body'
-
- $body.removeAttr 'data-page'
- $body.removeAttr 'data-project'
- $body.removeAttr 'data-group'
-
- switch section
- when 'dashboard'
- $body.data 'page', 'root:index'
- when 'group'
- $body.data 'page', 'groups:show'
- $body.data 'group', 'gitlab-org'
- when 'project'
- $body.data 'page', 'projects:show'
- $body.data 'project', 'gitlab-ce'
-
-
-# Mock `gl` object in window for dashboard specific page. App code will need it.
-mockDashboardOptions = ->
-
- window.gl or= {}
- window.gl.dashboardOptions =
- issuesPath: dashboardIssuesPath
- mrPath : dashboardMRsPath
-
-
-# Mock `gl` object in window for project specific page. App code will need it.
-mockProjectOptions = ->
-
- window.gl or= {}
- window.gl.projectOptions =
- 'gitlab-ce' :
- issuesPath : projectIssuesPath
- mrPath : projectMRsPath
- projectName : projectName
-
-
-mockGroupOptions = ->
-
- window.gl or= {}
- window.gl.groupOptions =
- 'gitlab-org' :
- issuesPath : groupIssuesPath
- mrPath : groupMRsPath
- projectName : groupName
-
-
-assertLinks = (list, issuesPath, mrsPath) ->
-
- issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}"
- issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}"
- mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}"
- mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}"
-
- a1 = "a[href='#{issuesAssignedToMeLink}']"
- a2 = "a[href='#{issuesIHaveCreatedLink}']"
- a3 = "a[href='#{mrsAssignedToMeLink}']"
- a4 = "a[href='#{mrsIHaveCreatedLink}']"
-
- expect(list.find(a1).length).toBe 1
- expect(list.find(a1).text()).toBe ' Issues assigned to me '
-
- expect(list.find(a2).length).toBe 1
- expect(list.find(a2).text()).toBe " Issues I've created "
-
- expect(list.find(a3).length).toBe 1
- expect(list.find(a3).text()).toBe ' Merge requests assigned to me '
-
- expect(list.find(a4).length).toBe 1
- expect(list.find(a4).text()).toBe " Merge requests I've created "
-
-
-describe 'Search autocomplete dropdown', ->
-
- fixture.preload 'search_autocomplete.html'
-
- beforeEach ->
-
- fixture.load 'search_autocomplete.html'
- widget = new SearchAutocomplete
-
-
- it 'should show Dashboard specific dropdown menu', ->
-
- addBodyAttributes()
- mockDashboardOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, dashboardIssuesPath, dashboardMRsPath
-
-
- it 'should show Group specific dropdown menu', ->
-
- addBodyAttributes 'group'
- mockGroupOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, groupIssuesPath, groupMRsPath
-
-
- it 'should show Project specific dropdown menu', ->
-
- addBodyAttributes 'project'
- mockProjectOptions()
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- assertLinks list, projectIssuesPath, projectMRsPath
-
-
- it 'should not show category related menu if there is text in the input', ->
-
- addBodyAttributes 'project'
- mockProjectOptions()
- widget.searchInput.val 'help'
- widget.searchInput.focus()
-
- list = widget.wrap.find('.dropdown-menu').find 'ul'
- link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']"
- expect(list.find(link).length).toBe 0
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
new file mode 100644
index 00000000000..7b6b55fe545
--- /dev/null
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -0,0 +1,74 @@
+
+/*= require shortcuts_issuable */
+
+(function() {
+ describe('ShortcutsIssuable', function() {
+ fixture.preload('issuable.html');
+ beforeEach(function() {
+ fixture.load('issuable.html');
+ return this.shortcut = new ShortcutsIssuable();
+ });
+ return describe('#replyWithSelectedText', function() {
+ var stubSelection;
+ stubSelection = function(text) {
+ return window.getSelection = function() {
+ return text;
+ };
+ };
+ beforeEach(function() {
+ return this.selector = 'form.js-main-target-form textarea#note_note';
+ });
+ describe('with empty selection', function() {
+ return it('does nothing', function() {
+ stubSelection('');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe('');
+ });
+ });
+ describe('with any selection', function() {
+ beforeEach(function() {
+ return stubSelection('Selected text.');
+ });
+ it('leaves existing input intact', function() {
+ $(this.selector).val('This text was already here.');
+ expect($(this.selector).val()).toBe('This text was already here.');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n");
+ });
+ it('triggers `input`', function() {
+ var triggered;
+ triggered = false;
+ $(this.selector).on('input', function() {
+ return triggered = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ return expect(triggered).toBe(true);
+ });
+ return it('triggers `focus`', function() {
+ var focused;
+ focused = false;
+ $(this.selector).on('focus', function() {
+ return focused = true;
+ });
+ this.shortcut.replyWithSelectedText();
+ return expect(focused).toBe(true);
+ });
+ });
+ describe('with a one-line selection', function() {
+ return it('quotes the selection', function() {
+ stubSelection('This text has been selected.');
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("> This text has been selected.\n\n");
+ });
+ });
+ return describe('with a multi-line selection', function() {
+ return it('quotes the selected lines as a group', function() {
+ stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n");
+ this.shortcut.replyWithSelectedText();
+ return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n");
+ });
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee
deleted file mode 100644
index a01ad7140dd..00000000000
--- a/spec/javascripts/shortcuts_issuable_spec.js.coffee
+++ /dev/null
@@ -1,82 +0,0 @@
-#= require shortcuts_issuable
-
-describe 'ShortcutsIssuable', ->
- fixture.preload('issuable.html')
-
- beforeEach ->
- fixture.load('issuable.html')
- @shortcut = new ShortcutsIssuable()
-
- describe '#replyWithSelectedText', ->
- # Stub window.getSelection to return the provided String.
- stubSelection = (text) ->
- window.getSelection = -> text
-
- beforeEach ->
- @selector = 'form.js-main-target-form textarea#note_note'
-
- describe 'with empty selection', ->
- it 'does nothing', ->
- stubSelection('')
- @shortcut.replyWithSelectedText()
- expect($(@selector).val()).toBe('')
-
- describe 'with any selection', ->
- beforeEach ->
- stubSelection('Selected text.')
-
- it 'leaves existing input intact', ->
- $(@selector).val('This text was already here.')
- expect($(@selector).val()).toBe('This text was already here.')
-
- @shortcut.replyWithSelectedText()
- expect($(@selector).val()).
- toBe("This text was already here.\n> Selected text.\n\n")
-
- it 'triggers `input`', ->
- triggered = false
- $(@selector).on 'input', -> triggered = true
- @shortcut.replyWithSelectedText()
-
- expect(triggered).toBe(true)
-
- it 'triggers `focus`', ->
- focused = false
- $(@selector).on 'focus', -> focused = true
- @shortcut.replyWithSelectedText()
-
- expect(focused).toBe(true)
-
- describe 'with a one-line selection', ->
- it 'quotes the selection', ->
- stubSelection('This text has been selected.')
-
- @shortcut.replyWithSelectedText()
-
- expect($(@selector).val()).
- toBe("> This text has been selected.\n\n")
-
- describe 'with a multi-line selection', ->
- it 'quotes the selected lines as a group', ->
- stubSelection(
- """
- Selected line one.
-
- Selected line two.
- Selected line three.
-
- """
- )
-
- @shortcut.replyWithSelectedText()
-
- expect($(@selector).val()).
- toBe(
- """
- > Selected line one.
- > Selected line two.
- > Selected line three.
-
-
- """
- )
diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee
deleted file mode 100644
index 90b02a6aec5..00000000000
--- a/spec/javascripts/spec_helper.coffee
+++ /dev/null
@@ -1,47 +0,0 @@
-# PhantomJS (Teaspoons default driver) doesn't have support for
-# Function.prototype.bind, which has caused confusion. Use this polyfill to
-# avoid the confusion.
-
-#= require support/bind-poly
-
-# You can require your own javascript files here. By default this will include
-# everything in application, however you may get better load performance if you
-# require the specific files that are being used in the spec that tests them.
-
-#= require jquery
-#= require jquery.turbolinks
-#= require bootstrap
-#= require underscore
-
-# Teaspoon includes some support files, but you can use anything from your own
-# support path too.
-
-# require support/jasmine-jquery-1.7.0
-# require support/jasmine-jquery-2.0.0
-#= require support/jasmine-jquery-2.1.0
-# require support/sinon
-# require support/your-support-file
-
-# Deferring execution
-
-# If you're using CommonJS, RequireJS or some other asynchronous library you can
-# defer execution. Call Teaspoon.execute() after everything has been loaded.
-# Simple example of a timeout:
-
-# Teaspoon.defer = true
-# setTimeout(Teaspoon.execute, 1000)
-
-# Matching files
-
-# By default Teaspoon will look for files that match
-# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path
-# and it'll be included in the default suite automatically. If you want to
-# customize suites, check out the configuration in teaspoon_env.rb
-
-# Manifest
-
-# If you'd rather require your spec files manually (to control order for
-# instance) you can disable the suite matcher in the configuration and use this
-# file as a manifest.
-
-# For more information: http://github.com/modeset/teaspoon
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
new file mode 100644
index 00000000000..7d91ed0f855
--- /dev/null
+++ b/spec/javascripts/spec_helper.js
@@ -0,0 +1,22 @@
+
+/*= require support/bind-poly */
+
+
+/*= require jquery */
+
+
+/*= require jquery.turbolinks */
+
+
+/*= require bootstrap */
+
+
+/*= require underscore */
+
+
+/*= require support/jasmine-jquery-2.1.0 */
+
+(function() {
+
+
+}).call(this);
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
new file mode 100644
index 00000000000..4e5dd1e59bf
--- /dev/null
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -0,0 +1,44 @@
+
+/*= require syntax_highlight */
+
+(function() {
+ describe('Syntax Highlighter', function() {
+ var stubUserColorScheme;
+ stubUserColorScheme = function(value) {
+ if (window.gon == null) {
+ window.gon = {};
+ }
+ return window.gon.user_color_scheme = value;
+ };
+ describe('on a js-syntax-highlight element', function() {
+ beforeEach(function() {
+ return fixture.set('<div class="js-syntax-highlight"></div>');
+ });
+ return it('applies syntax highlighting', function() {
+ stubUserColorScheme('monokai');
+ $('.js-syntax-highlight').syntaxHighlight();
+ return expect($('.js-syntax-highlight')).toHaveClass('monokai');
+ });
+ });
+ return describe('on a parent element', function() {
+ beforeEach(function() {
+ return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>");
+ });
+ it('applies highlighting to all applicable children', function() {
+ stubUserColorScheme('monokai');
+ $('.parent').syntaxHighlight();
+ expect($('.parent, .foo')).not.toHaveClass('monokai');
+ return expect($('.monokai').length).toBe(2);
+ });
+ return it('prevents an infinite loop when no matches exist', function() {
+ var highlight;
+ fixture.set('<div></div>');
+ highlight = function() {
+ return $('div').syntaxHighlight();
+ };
+ return expect(highlight).not.toThrow();
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee
deleted file mode 100644
index 6a73b6bf32c..00000000000
--- a/spec/javascripts/syntax_highlight_spec.js.coffee
+++ /dev/null
@@ -1,42 +0,0 @@
-#= require syntax_highlight
-
-describe 'Syntax Highlighter', ->
- stubUserColorScheme = (value) ->
- window.gon ?= {}
- window.gon.user_color_scheme = value
-
- describe 'on a js-syntax-highlight element', ->
- beforeEach ->
- fixture.set('<div class="js-syntax-highlight"></div>')
-
- it 'applies syntax highlighting', ->
- stubUserColorScheme('monokai')
-
- $('.js-syntax-highlight').syntaxHighlight()
-
- expect($('.js-syntax-highlight')).toHaveClass('monokai')
-
- describe 'on a parent element', ->
- beforeEach ->
- fixture.set """
- <div class="parent">
- <div class="js-syntax-highlight"></div>
- <div class="foo"></div>
- <div class="js-syntax-highlight"></div>
- </div>
- """
-
- it 'applies highlighting to all applicable children', ->
- stubUserColorScheme('monokai')
-
- $('.parent').syntaxHighlight()
-
- expect($('.parent, .foo')).not.toHaveClass('monokai')
- expect($('.monokai').length).toBe(2)
-
- it 'prevents an infinite loop when no matches exist', ->
- fixture.set('<div></div>')
-
- highlight = -> $('div').syntaxHighlight()
-
- expect(highlight).not.toThrow()
diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee
deleted file mode 100644
index e8a2892d678..00000000000
--- a/spec/javascripts/u2f/authenticate_spec.coffee
+++ /dev/null
@@ -1,52 +0,0 @@
-#= require u2f/authenticate
-#= require u2f/util
-#= require u2f/error
-#= require u2f
-#= require ./mock_u2f_device
-
-describe 'U2FAuthenticate', ->
- U2FUtil.enableTestMode()
- fixture.load('u2f/authenticate')
-
- beforeEach ->
- @u2fDevice = new MockU2FDevice
- @container = $("#js-authenticate-u2f")
- @component = new U2FAuthenticate(@container, {}, "token")
- @component.start()
-
- it 'allows authenticating via a U2F device', ->
- setupButton = @container.find("#js-login-u2f-device")
- setupMessage = @container.find("p")
- expect(setupMessage.text()).toContain('Insert your security key')
- expect(setupButton.text()).toBe('Login Via U2F Device')
- setupButton.trigger('click')
-
- inProgressMessage = @container.find("p")
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
-
- @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
- authenticatedMessage = @container.find("p")
- deviceResponse = @container.find('#js-device-response')
- expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
- expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
-
- describe "errors", ->
- it "displays an error message", ->
- setupButton = @container.find("#js-login-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
- errorMessage = @container.find("p")
- expect(errorMessage.text()).toContain("There was a problem communicating with your device")
-
- it "allows retrying authentication after an error", ->
- setupButton = @container.find("#js-login-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"})
- retryButton = @container.find("#js-u2f-try-again")
- retryButton.trigger('click')
-
- setupButton = @container.find("#js-login-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"})
- authenticatedMessage = @container.find("p")
- expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server")
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
new file mode 100644
index 00000000000..e008ce956ad
--- /dev/null
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -0,0 +1,75 @@
+
+/*= require u2f/authenticate */
+
+
+/*= require u2f/util */
+
+
+/*= require u2f/error */
+
+
+/*= require u2f */
+
+
+/*= require ./mock_u2f_device */
+
+(function() {
+ describe('U2FAuthenticate', function() {
+ fixture.load('u2f/authenticate');
+ beforeEach(function() {
+ this.u2fDevice = new MockU2FDevice;
+ this.container = $("#js-authenticate-u2f");
+ this.component = new U2FAuthenticate(this.container, {
+ sign_requests: []
+ }, "token");
+ return this.component.start();
+ });
+ it('allows authenticating via a U2F device', function() {
+ var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupMessage = this.container.find("p");
+ expect(setupMessage.text()).toContain('Insert your security key');
+ expect(setupButton.text()).toBe('Login Via U2F Device');
+ setupButton.trigger('click');
+ inProgressMessage = this.container.find("p");
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: "this is data from the device"
+ });
+ authenticatedMessage = this.container.find("p");
+ deviceResponse = this.container.find('#js-device-response');
+ expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server");
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+ return describe("errors", function() {
+ it("displays an error message", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: "error!"
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ });
+ return it("allows retrying authentication after an error", function() {
+ var authenticatedMessage, retryButton, setupButton;
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ errorCode: "error!"
+ });
+ retryButton = this.container.find("#js-u2f-try-again");
+ retryButton.trigger('click');
+ setupButton = this.container.find("#js-login-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToAuthenticateRequest({
+ deviceData: "this is data from the device"
+ });
+ authenticatedMessage = this.container.find("p");
+ return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server");
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
new file mode 100644
index 00000000000..ca91a716ba3
--- /dev/null
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -0,0 +1,33 @@
+(function() {
+ var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+
+ this.MockU2FDevice = (function() {
+ function MockU2FDevice() {
+ this.respondToAuthenticateRequest = bind(this.respondToAuthenticateRequest, this);
+ this.respondToRegisterRequest = bind(this.respondToRegisterRequest, this);
+ window.u2f || (window.u2f = {});
+ window.u2f.register = (function(_this) {
+ return function(appId, registerRequests, signRequests, callback) {
+ return _this.registerCallback = callback;
+ };
+ })(this);
+ window.u2f.sign = (function(_this) {
+ return function(appId, challenges, signRequests, callback) {
+ return _this.authenticateCallback = callback;
+ };
+ })(this);
+ }
+
+ MockU2FDevice.prototype.respondToRegisterRequest = function(params) {
+ return this.registerCallback(params);
+ };
+
+ MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) {
+ return this.authenticateCallback(params);
+ };
+
+ return MockU2FDevice;
+
+ })();
+
+}).call(this);
diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee
deleted file mode 100644
index 97ed0e83a0e..00000000000
--- a/spec/javascripts/u2f/mock_u2f_device.js.coffee
+++ /dev/null
@@ -1,15 +0,0 @@
-class @MockU2FDevice
- constructor: () ->
- window.u2f ||= {}
-
- window.u2f.register = (appId, registerRequests, signRequests, callback) =>
- @registerCallback = callback
-
- window.u2f.sign = (appId, challenges, signRequests, callback) =>
- @authenticateCallback = callback
-
- respondToRegisterRequest: (params) =>
- @registerCallback(params)
-
- respondToAuthenticateRequest: (params) =>
- @authenticateCallback(params)
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
new file mode 100644
index 00000000000..21c5266c60e
--- /dev/null
+++ b/spec/javascripts/u2f/register_spec.js
@@ -0,0 +1,81 @@
+
+/*= require u2f/register */
+
+
+/*= require u2f/util */
+
+
+/*= require u2f/error */
+
+
+/*= require u2f */
+
+
+/*= require ./mock_u2f_device */
+
+(function() {
+ describe('U2FRegister', function() {
+ fixture.load('u2f/register');
+ beforeEach(function() {
+ this.u2fDevice = new MockU2FDevice;
+ this.container = $("#js-register-u2f");
+ this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token");
+ return this.component.start();
+ });
+ it('allows registering a U2F device', function() {
+ var deviceResponse, inProgressMessage, registeredMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ expect(setupButton.text()).toBe('Setup New U2F Device');
+ setupButton.trigger('click');
+ inProgressMessage = this.container.children("p");
+ expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: "this is data from the device"
+ });
+ registeredMessage = this.container.find('p');
+ deviceResponse = this.container.find('#js-device-response');
+ expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}');
+ });
+ return describe("errors", function() {
+ it("doesn't allow the same device to be registered twice (for the same user", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: 4
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("already been registered with us");
+ });
+ it("displays an error message for other errors", function() {
+ var errorMessage, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: "error!"
+ });
+ errorMessage = this.container.find("p");
+ return expect(errorMessage.text()).toContain("There was a problem communicating with your device");
+ });
+ return it("allows retrying registration after an error", function() {
+ var registeredMessage, retryButton, setupButton;
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ errorCode: "error!"
+ });
+ retryButton = this.container.find("#U2FTryAgain");
+ retryButton.trigger('click');
+ setupButton = this.container.find("#js-setup-u2f-device");
+ setupButton.trigger('click');
+ this.u2fDevice.respondToRegisterRequest({
+ deviceData: "this is data from the device"
+ });
+ registeredMessage = this.container.find("p");
+ return expect(registeredMessage.text()).toContain("Your device was successfully set up!");
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee
deleted file mode 100644
index 0858abeca1a..00000000000
--- a/spec/javascripts/u2f/register_spec.js.coffee
+++ /dev/null
@@ -1,57 +0,0 @@
-#= require u2f/register
-#= require u2f/util
-#= require u2f/error
-#= require u2f
-#= require ./mock_u2f_device
-
-describe 'U2FRegister', ->
- U2FUtil.enableTestMode()
- fixture.load('u2f/register')
-
- beforeEach ->
- @u2fDevice = new MockU2FDevice
- @container = $("#js-register-u2f")
- @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token")
- @component.start()
-
- it 'allows registering a U2F device', ->
- setupButton = @container.find("#js-setup-u2f-device")
- expect(setupButton.text()).toBe('Setup New U2F Device')
- setupButton.trigger('click')
-
- inProgressMessage = @container.children("p")
- expect(inProgressMessage.text()).toContain("Trying to communicate with your device")
-
- @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
- registeredMessage = @container.find('p')
- deviceResponse = @container.find('#js-device-response')
- expect(registeredMessage.text()).toContain("Your device was successfully set up!")
- expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}')
-
- describe "errors", ->
- it "doesn't allow the same device to be registered twice (for the same user", ->
- setupButton = @container.find("#js-setup-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToRegisterRequest({errorCode: 4})
- errorMessage = @container.find("p")
- expect(errorMessage.text()).toContain("already been registered with us")
-
- it "displays an error message for other errors", ->
- setupButton = @container.find("#js-setup-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
- errorMessage = @container.find("p")
- expect(errorMessage.text()).toContain("There was a problem communicating with your device")
-
- it "allows retrying registration after an error", ->
- setupButton = @container.find("#js-setup-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToRegisterRequest({errorCode: "error!"})
- retryButton = @container.find("#U2FTryAgain")
- retryButton.trigger('click')
-
- setupButton = @container.find("#js-setup-u2f-device")
- setupButton.trigger('click')
- @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"})
- registeredMessage = @container.find("p")
- expect(registeredMessage.text()).toContain("Your device was successfully set up!")
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
new file mode 100644
index 00000000000..3d680ec8ea3
--- /dev/null
+++ b/spec/javascripts/zen_mode_spec.js
@@ -0,0 +1,73 @@
+
+/*= require zen_mode */
+
+(function() {
+ var enterZen, escapeKeydown, exitZen;
+
+ describe('ZenMode', function() {
+ fixture.preload('zen_mode.html');
+ beforeEach(function() {
+ fixture.load('zen_mode.html');
+ spyOn(Dropzone, 'forElement').and.callFake(function() {
+ return {
+ enable: function() {
+ return true;
+ }
+ };
+ });
+ this.zen = new ZenMode();
+ return this.zen.scroll_position = 456;
+ });
+ describe('on enter', function() {
+ it('pauses Mousetrap', function() {
+ spyOn(Mousetrap, 'pause');
+ enterZen();
+ return expect(Mousetrap.pause).toHaveBeenCalled();
+ });
+ return it('removes textarea styling', function() {
+ $('textarea').attr('style', 'height: 400px');
+ enterZen();
+ return expect('textarea').not.toHaveAttr('style');
+ });
+ });
+ describe('in use', function() {
+ beforeEach(function() {
+ return enterZen();
+ });
+ return it('exits on Escape', function() {
+ escapeKeydown();
+ return expect($('.zen-backdrop')).not.toHaveClass('fullscreen');
+ });
+ });
+ return describe('on exit', function() {
+ beforeEach(function() {
+ return enterZen();
+ });
+ it('unpauses Mousetrap', function() {
+ spyOn(Mousetrap, 'unpause');
+ exitZen();
+ return expect(Mousetrap.unpause).toHaveBeenCalled();
+ });
+ return it('restores the scroll position', function() {
+ spyOn(this.zen, 'scrollTo');
+ exitZen();
+ return expect(this.zen.scrollTo).toHaveBeenCalled();
+ });
+ });
+ });
+
+ enterZen = function() {
+ return $('a.js-zen-enter').click();
+ };
+
+ exitZen = function() {
+ return $('a.js-zen-leave').click();
+ };
+
+ escapeKeydown = function() {
+ return $('textarea').trigger($.Event('keydown', {
+ keyCode: 27
+ }));
+ };
+
+}).call(this);
diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee
deleted file mode 100644
index b790fce01ed..00000000000
--- a/spec/javascripts/zen_mode_spec.js.coffee
+++ /dev/null
@@ -1,51 +0,0 @@
-#= require zen_mode
-
-describe 'ZenMode', ->
- fixture.preload('zen_mode.html')
-
- beforeEach ->
- fixture.load('zen_mode.html')
-
- # Stub Dropzone.forElement(...).enable()
- spyOn(Dropzone, 'forElement').and.callFake ->
- enable: -> true
-
- @zen = new ZenMode()
-
- # Set this manually because we can't actually scroll the window
- @zen.scroll_position = 456
-
- describe 'on enter', ->
- it 'pauses Mousetrap', ->
- spyOn(Mousetrap, 'pause')
- enterZen()
- expect(Mousetrap.pause).toHaveBeenCalled()
-
- it 'removes textarea styling', ->
- $('textarea').attr('style', 'height: 400px')
- enterZen()
- expect('textarea').not.toHaveAttr('style')
-
- describe 'in use', ->
- beforeEach -> enterZen()
-
- it 'exits on Escape', ->
- escapeKeydown()
- expect($('.zen-backdrop')).not.toHaveClass('fullscreen')
-
- describe 'on exit', ->
- beforeEach -> enterZen()
-
- it 'unpauses Mousetrap', ->
- spyOn(Mousetrap, 'unpause')
- exitZen()
- expect(Mousetrap.unpause).toHaveBeenCalled()
-
- it 'restores the scroll position', ->
- spyOn(@zen, 'scrollTo')
- exitZen()
- expect(@zen.scrollTo).toHaveBeenCalled()
-
-enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm
-exitZen = -> $('a.js-zen-leave').click()
-escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27}))
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index 84c2ddf444e..dca7f997570 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -15,6 +15,16 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
+ context 'when the input contains no links' do
+ it 'does not parse_html back the rinku returned value' do
+ act = HTML::Pipeline.parse('<p>This text contains no links to autolink</p>')
+
+ expect_any_instance_of(described_class).not_to receive(:parse_html)
+
+ filter(act).to_html
+ end
+ end
+
context 'Rinku schemes' do
it 'autolinks http' do
doc = filter("See #{link}")
@@ -58,6 +68,16 @@ describe Banzai::Filter::AutolinkFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
end
+
+ context 'when the input contains link' do
+ it 'does parse_html back the rinku returned value' do
+ act = HTML::Pipeline.parse("<p>See #{link}</p>")
+
+ expect_any_instance_of(described_class).to receive(:parse_html).at_least(:once).and_call_original
+
+ filter(act).to_html
+ end
+ end
end
context 'other schemes' do
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 9e3d2f5825d..9276a154007 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -93,8 +93,8 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.))
end
it 'ignores invalid label names' do
@@ -104,8 +104,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based single-word references that begin with a digit' do
+ let(:label) { create(:label, name: '2fa', project: project) }
+ let(:reference) { "#{Label.reference_prefix}#{label.name}" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See 2fa'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\)\.))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label #{Label.reference_prefix}#{label.id}#{label.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
context 'String-based single-word references with special characters' do
- let(:label) { create(:label, name: '?gfm&', project: project) }
+ let(:label) { create(:label, name: '?g.fm&', project: project) }
let(:reference) { "#{Label.reference_prefix}#{label.name}" }
it 'links to a valid reference' do
@@ -113,17 +137,17 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
- expect(doc.text).to eq 'See ?gfm&'
+ expect(doc.text).to eq 'See ?g.fm&'
end
it 'links with adjacent text' do
- doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>\?gfm&amp;</span></a>\.\)))
+ doc = reference_filter("Label (#{reference}).")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>\?g\.fm&amp;</span></a>\)\.))
end
it 'ignores invalid label names' do
act = "Label #{Label.reference_prefix}#{label.name.reverse}"
- exp = "Label #{Label.reference_prefix}&amp;mfg?"
+ exp = "Label #{Label.reference_prefix}&amp;mf.g?"
expect(reference_filter(act).to_html).to eq exp
end
@@ -153,8 +177,32 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
end
end
+ context 'String-based multi-word references that begin with a digit' do
+ let(:label) { create(:label, name: '2 factor authentication', project: project) }
+ let(:reference) { label.to_reference(format: :name) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See 2 factor authentication'
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label #{Label.reference_prefix}#{label.id}#{label.name.reverse}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
context 'String-based multi-word references with special characters in quotes' do
- let(:label) { create(:label, name: 'gfm & references?', project: project) }
+ let(:label) { create(:label, name: 'g.fm & references?', project: project) }
let(:reference) { label.to_reference(format: :name) }
it 'links to a valid reference' do
@@ -162,22 +210,62 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do
expect(doc.css('a').first.attr('href')).to eq urls.
namespace_project_issues_url(project.namespace, project, label_name: label.name)
- expect(doc.text).to eq 'See gfm & references?'
+ expect(doc.text).to eq 'See g.fm & references?'
end
it 'links with adjacent text' do
doc = reference_filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>gfm &amp; references\?</span></a>\.\)))
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>g\.fm &amp; references\?</span></a>\.\)))
end
it 'ignores invalid label names' do
act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
- exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mfg\")
+ exp = %(Label #{Label.reference_prefix}"?secnerefer &amp; mf.g\")
expect(reference_filter(act).to_html).to eq exp
end
end
+ describe 'consecutive references' do
+ let(:bug) { create(:label, name: 'bug', project: project) }
+ let(:feature_proposal) { create(:label, name: 'feature proposal', project: project) }
+ let(:technical_debt) { create(:label, name: 'technical debt', project: project) }
+
+ let(:bug_reference) { "#{Label.reference_prefix}#{bug.name}" }
+ let(:feature_proposal_reference) { feature_proposal.to_reference(format: :name) }
+ let(:technical_debt_reference) { technical_debt.to_reference(format: :name) }
+
+ context 'separated with a comma' do
+ let(:references) { "#{bug_reference}, #{feature_proposal_reference}, #{technical_debt_reference}" }
+
+ it 'links to valid references' do
+ doc = reference_filter("See #{references}")
+
+ expect(doc.css('a').map { |a| a.attr('href') }).to match_array([
+ urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name)
+ ])
+ expect(doc.text).to eq 'See bug, feature proposal, technical debt'
+ end
+ end
+
+ context 'separated with a space' do
+ let(:references) { "#{bug_reference} #{feature_proposal_reference} #{technical_debt_reference}" }
+
+ it 'links to valid references' do
+ doc = reference_filter("See #{references}")
+
+ expect(doc.css('a').map { |a| a.attr('href') }).to match_array([
+ urls.namespace_project_issues_url(project.namespace, project, label_name: bug.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: feature_proposal.name),
+ urls.namespace_project_issues_url(project.namespace, project, label_name: technical_debt.name)
+ ])
+ expect(doc.text).to eq 'See bug feature proposal technical debt'
+ end
+ end
+ end
+
describe 'edge cases' do
it 'gracefully handles non-references matching the pattern' do
exp = act = '(format nil "~0f" 3.0) ; 3.0'
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index b9e4a4eaf0e..6b58f3e43ee 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -1,11 +1,9 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::RelativeLinkFilter, lib: true do
def filter(doc, contexts = {})
contexts.reverse_merge!({
- commit: project.commit,
+ commit: commit,
project: project,
project_wiki: project_wiki,
ref: ref,
@@ -19,6 +17,10 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
%(<img src="#{path}" />)
end
+ def video(path)
+ %(<video src="#{path}"></video>)
+ end
+
def link(path)
%(<a href="#{path}">#{path}</a>)
end
@@ -26,6 +28,7 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
let(:project) { create(:project) }
let(:project_path) { project.path_with_namespace }
let(:ref) { 'markdown' }
+ let(:commit) { project.commit(ref) }
let(:project_wiki) { nil }
let(:requested_path) { '/' }
@@ -39,6 +42,12 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
doc = filter(image('files/images/logo-black.png'))
expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
end
+
+ it 'does not modify any relative URL in video' do
+ doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video')
+
+ expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4'
+ end
end
shared_examples :relative_to_requested do
@@ -69,13 +78,36 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
expect { filter(act) }.not_to raise_error
end
- context 'with a valid repository' do
+ it 'ignores ref if commit is passed' do
+ doc = filter(link('non/existent.file'), commit: project.commit('empty-branch') )
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/#{ref}/non/existent.file" # non-existent files have no leading blob/raw/tree
+ end
+
+ shared_examples :valid_repository do
+ it 'rebuilds absolute URL for a file in the repo' do
+ doc = filter(link('/doc/api/README.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'ignores absolute URLs with two leading slashes' do
+ doc = filter(link('//doc/api/README.md'))
+ expect(doc.at_css('a')['href']).to eq '//doc/api/README.md'
+ end
+
it 'rebuilds relative URL for a file in the repo' do
doc = filter(link('doc/api/README.md'))
expect(doc.at_css('a')['href']).
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repo with leading ./' do
+ doc = filter(link('./doc/api/README.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo up one directory' do
relative_link = link('../api/README.md')
doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
@@ -113,11 +145,26 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
end
it 'rebuilds relative URL for an image in the repo' do
+ doc = filter(image('files/images/logo-black.png'))
+
+ expect(doc.at_css('img')['src']).
+ to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
+ end
+
+ it 'rebuilds relative URL for link to an image in the repo' do
doc = filter(link('files/images/logo-black.png'))
+
expect(doc.at_css('a')['href']).
to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
end
+ it 'rebuilds relative URL for a video in the repo' do
+ doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video')
+
+ expect(doc.at_css('video')['src']).
+ to eq "/#{project_path}/raw/video/files/videos/intro.mp4"
+ end
+
it 'does not modify relative URL with an anchor only' do
doc = filter(link('#section-1'))
expect(doc.at_css('a')['href']).to eq '#section-1'
@@ -149,4 +196,13 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
include_examples :relative_to_requested
end
end
+
+ context 'with a valid commit' do
+ include_examples :valid_repository
+ end
+
+ context 'with a valid ref' do
+ let(:commit) { nil } # force filter to use ref instead of commit
+ include_examples :valid_repository
+ end
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 407617f3307..b1370bca833 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -3,15 +3,35 @@ require 'spec_helper'
describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
include FilterSpecHelper
- it 'highlights valid code blocks' do
- result = filter('<pre><code>def fun end</code>')
- expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
+ context "when no language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>')
+ end
end
- it 'passes through invalid code blocks' do
- allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError)
+ context "when a valid language is specified" do
+ it "highlights as that language" do
+ result = filter('<pre><code class="ruby">def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ end
+ end
+
+ context "when an invalid language is specified" do
+ it "highlights as plaintext" do
+ result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>')
+ end
+ end
+
+ context "when Rouge formatting fails" do
+ before do
+ allow_any_instance_of(Rouge::Formatter).to receive(:format).and_raise(StandardError)
+ end
- result = filter('<pre><code>This is a test</code></pre>')
- expect(result.to_html).to eq('<pre>This is a test</pre>')
+ it "highlights as plaintext" do
+ result = filter('<pre><code class="ruby">This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>')
+ end
end
end
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 6a5d003e87f..356dd01a03a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -1,5 +1,3 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::TableOfContentsFilter, lib: true do
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 273d2ed709a..8b76c1d73c9 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -1,5 +1,3 @@
-# encoding: UTF-8
-
require 'spec_helper'
describe Banzai::Filter::UploadLinkFilter, lib: true do
diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb
new file mode 100644
index 00000000000..6ab1be9ccb7
--- /dev/null
+++ b/spec/lib/banzai/filter/video_link_filter_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Banzai::Filter::VideoLinkFilter, lib: true do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ project: project
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def link_to_image(path)
+ %(<img src="#{path}" />)
+ end
+
+ let(:project) { create(:project) }
+
+ context 'when the element src has a video extension' do
+ UploaderHelper::VIDEO_EXT.each do |ext|
+ it "replaces the image tag 'path/video.#{ext}' with a video tag" do
+ container = filter(link_to_image("/path/video.#{ext}")).children.first
+
+ expect(container.name).to eq 'div'
+ expect(container['class']).to eq 'video-container'
+
+ video, paragraph = container.children
+
+ expect(video.name).to eq 'video'
+ expect(video['src']).to eq "/path/video.#{ext}"
+
+ expect(paragraph.name).to eq 'p'
+
+ link = paragraph.children.first
+
+ expect(link.name).to eq 'a'
+ expect(link['href']).to eq "/path/video.#{ext}"
+ expect(link['target']).to eq '_blank'
+ end
+ end
+ end
+
+ context 'when the element src is an image' do
+ it 'leaves the document unchanged' do
+ element = filter(link_to_image('/path/my_image.jpg')).children.first
+
+ expect(element.name).to eq 'img'
+ expect(element['src']).to eq '/path/my_image.jpg'
+ end
+ end
+end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 44256b32bdc..bcdb95250ca 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -17,6 +17,7 @@ describe Banzai::ObjectRenderer do
and_call_original
expect(object).to receive(:note_html=).with('<p>hello</p>')
+ expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note)
end
@@ -25,9 +26,10 @@ describe Banzai::ObjectRenderer do
describe '#render_objects' do
it 'renders an Array of objects' do
object = double(:object, note: 'hello')
+
renderer = described_class.new(project, user)
- expect(renderer).to receive(:render_attribute).with(object, :note).
+ expect(renderer).to receive(:render_attributes).with([object], :note).
and_call_original
rendered = renderer.render_objects([object], :note)
@@ -38,7 +40,7 @@ describe Banzai::ObjectRenderer do
end
describe '#redact_documents' do
- it 'redacts a set of documents and returns them as an Array of Strings' do
+ it 'redacts a set of documents and returns them as an Array of Hashes' do
doc = Nokogiri::HTML.fragment('<p>hello</p>')
renderer = described_class.new(project, user)
@@ -48,7 +50,9 @@ describe Banzai::ObjectRenderer do
redacted = renderer.redact_documents([doc])
- expect(redacted).to eq(['<p>hello</p>'])
+ expect(redacted.count).to eq(1)
+ expect(redacted.first[:visible_reference_count]).to eq(0)
+ expect(redacted.first[:document].to_html).to eq('<p>hello</p>')
end
end
@@ -85,14 +89,36 @@ describe Banzai::ObjectRenderer do
end
end
- describe '#render_attribute' do
- it 'renders the attribute of an object' do
- object = double(:doc, note: 'hello')
+ describe '#render_attributes' do
+ it 'renders the attribute of a list of objects' do
+ objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
renderer = described_class.new(project, user, pipeline: :note)
- doc = renderer.render_attribute(object, :note)
- expect(doc).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
- expect(doc.to_html).to eq('<p>hello</p>')
+ expect(Banzai).to receive(:cache_collection_render).
+ with([
+ { text: 'hello', context: renderer.context_for(objects[0], :note) },
+ { text: 'bye', context: renderer.context_for(objects[1], :note) }
+ ]).
+ and_call_original
+
+ docs = renderer.render_attributes(objects, :note)
+
+ expect(docs[0]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ expect(docs[0].to_html).to eq('<p>hello</p>')
+
+ expect(docs[1]).to be_an_instance_of(Nokogiri::HTML::DocumentFragment)
+ expect(docs[1].to_html).to eq('<p>bye</p>')
+ end
+
+ it 'returns when no objects to render' do
+ objects = []
+ renderer = described_class.new(project, user, pipeline: :note)
+
+ expect(Banzai).to receive(:cache_collection_render).
+ with([]).
+ and_call_original
+
+ expect(renderer.render_attributes(objects, :note)).to eq([])
end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 488f465bcda..254657a881d 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -15,11 +15,31 @@ describe Banzai::Redactor do
expect(redactor).to receive(:nodes_visible_to_user).and_return([])
- expect(redactor.redact([doc1, doc2])).to eq([doc1, doc2])
+ redacted_data = redactor.redact([doc1, doc2])
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
expect(doc1.to_html).to eq('foo')
expect(doc2.to_html).to eq('bar')
end
+
+ it 'does not redact an Array of documents' do
+ doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
+ doc1 = Nokogiri::HTML.fragment(doc1_html)
+
+ doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
+ doc2 = Nokogiri::HTML.fragment(doc2_html)
+
+ nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
+ expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
+
+ redacted_data = redactor.redact([doc1, doc2])
+
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
+ expect(doc1.to_html).to eq(doc1_html)
+ expect(doc2.to_html).to eq(doc2_html)
+ end
end
describe '#redact_nodes' do
@@ -31,7 +51,7 @@ describe Banzai::Redactor do
with([node]).
and_return(Set.new)
- redactor.redact_nodes([node])
+ redactor.redact_document_nodes([{ document: doc, nodes: [node] }])
expect(doc.to_html).to eq('foo')
end
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 514c752546d..85cfe728b6a 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do
end
it 'returns the nodes when the user can read the issue' do
- expect(Ability.abilities).to receive(:allowed?).
- with(user, :read_issue, issue).
- and_return(true)
+ expect(Ability).to receive(:issues_readable_by_user).
+ with([issue], user).
+ and_return([issue])
expect(subject.nodes_visible_to_user(user, [link])).to eq([link])
end
it 'returns an empty Array when the user can not read the issue' do
- expect(Ability.abilities).to receive(:allowed?).
- with(user, :read_issue, issue).
- and_return(false)
+ expect(Ability).to receive(:issues_readable_by_user).
+ with([issue], user).
+ and_return([])
expect(subject.nodes_visible_to_user(user, [link])).to eq([])
end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 97f2e97b062..fb6cc398307 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -2,21 +2,23 @@ require 'spec_helper'
describe Ci::Charts, lib: true do
context "build_times" do
+ let(:project) { create(:empty_project) }
+ let(:chart) { Ci::Charts::BuildTime.new(project) }
+
+ subject { chart.build_times }
+
before do
- @pipeline = FactoryGirl.create(:ci_pipeline)
- FactoryGirl.create(:ci_build, pipeline: @pipeline)
+ create(:ci_empty_pipeline, project: project, duration: 120)
end
- it 'should return build times in minutes' do
- chart = Ci::Charts::BuildTime.new(@pipeline.project)
- expect(chart.build_times).to eq([2])
+ it 'returns build times in minutes' do
+ is_expected.to contain_exactly(2)
end
- it 'should handle nil build times' do
- create(:ci_pipeline, duration: nil, project: @pipeline.project)
+ it 'handles nil build times' do
+ create(:ci_empty_pipeline, project: project, duration: nil)
- chart = Ci::Charts::BuildTime.new(@pipeline.project)
- expect(chart.build_times).to eq([2, 0])
+ is_expected.to contain_exactly(2, 0)
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 bad439bc489..be51d942af7 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -19,19 +19,18 @@ module Ci
expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
stage: "test",
stage_idx: 1,
- except: nil,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {},
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
- describe :only do
+ describe 'only' do
it "does not return builds if only has another branch" do
config = YAML.dump({
before_script: ["pwd"],
@@ -163,7 +162,7 @@ module Ci
shared_examples 'raises an error' do
it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps')
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps')
end
end
@@ -187,7 +186,7 @@ module Ci
end
end
- describe :except do
+ describe 'except' do
it "returns builds if except has another branch" do
config = YAML.dump({
before_script: ["pwd"],
@@ -319,7 +318,7 @@ module Ci
shared_examples 'raises an error' do
it do
- expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps')
+ expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps')
end
end
@@ -432,11 +431,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -446,6 +443,7 @@ module Ci
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
@@ -461,11 +459,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -475,101 +471,122 @@ module Ci
allow_failure: false,
when: "on_success",
environment: nil,
+ yaml_variables: []
})
end
end
describe 'Variables' do
- context 'when global variables are defined' do
- it 'returns global variables' do
- variables = {
- VAR1: 'value1',
- VAR2: 'value2',
- }
+ let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }
- config = YAML.dump({
+ subject { config_processor.builds.first[:yaml_variables] }
+
+ context 'when global variables are defined' do
+ let(:variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
+ let(:config) do
+ {
variables: variables,
before_script: ['pwd'],
rspec: { script: 'rspec' }
- })
+ }
+ end
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ it 'returns global variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
+ end
+ end
- expect(config_processor.global_variables).to eq(variables)
+ context 'when job and global variables are defined' do
+ let(:global_variables) do
+ { VAR1: 'global1', VAR3: 'global3' }
+ end
+ let(:job_variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ variables: global_variables,
+ rspec: { script: 'rspec', variables: job_variables }
+ }
+ end
+
+ it 'returns all unique variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR3, value: 'global3', public: true },
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
end
end
context 'when job variables are defined' do
- context 'when syntax is correct' do
- it 'returns job variables' do
- variables = {
- KEY1: 'value1',
- SOME_KEY_2: 'value2'
- }
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ let(:config) do
+ {
+ before_script: ['pwd'],
+ rspec: { script: 'rspec', variables: variables }
+ }
+ end
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ context 'when syntax is correct' do
+ let(:variables) do
+ { VAR1: 'value1', VAR2: 'value2' }
+ end
- expect(config_processor.job_variables(:rspec)).to eq variables
+ it 'returns job variables' do
+ expect(subject).to contain_exactly(
+ { key: :VAR1, value: 'value1', public: true },
+ { key: :VAR2, value: 'value2', public: true }
+ )
end
end
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
- it 'raises error' do
- variables = [:KEY1, 'value1', :KEY2, 'value2']
-
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: variables,
- script: 'rspec' }
- })
+ let(:variables) do
+ [ :VAR1, 'value1', :VAR2, 'value2' ]
+ end
- expect { GitlabCiYamlProcessor.new(config, path) }
+ it 'raises error' do
+ expect { subject }
.to raise_error(GitlabCiYamlProcessor::ValidationError,
- /job: variables should be a map/)
+ /jobs:rspec:variables config should be a hash of key value pairs/)
end
end
context 'when variables key defined but value not specified' do
- it 'returns empty array' do
- config = YAML.dump(
- { before_script: ['pwd'],
- rspec: {
- variables: nil,
- script: 'rspec' }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ let(:variables) do
+ nil
+ end
+ it 'returns empty array' do
##
# When variables config is empty, we assume this is a valid
# configuration, see issue #18775
#
- expect(config_processor.job_variables(:rspec))
- .to be_an_instance_of(Array).and be_empty
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
end
end
end
end
context 'when job variables are not defined' do
- it 'returns empty array' do
- config = YAML.dump({
+ let(:config) do
+ {
before_script: ['pwd'],
rspec: { script: 'rspec' }
- })
-
- config_processor = GitlabCiYamlProcessor.new(config, path)
+ }
+ end
- expect(config_processor.job_variables(:rspec)).to eq []
+ it 'returns empty array' do
+ expect(subject).to be_an_instance_of(Array)
+ expect(subject).to be_empty
end
end
end
@@ -681,11 +698,9 @@ module Ci
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :rspec,
- only: nil,
+ name: "rspec",
commands: "pwd\nrspec",
tag_list: [],
options: {
@@ -701,6 +716,7 @@ module Ci
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
@@ -754,7 +770,7 @@ module Ci
let(:environment) { 1 }
it 'raises error' do
- expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
end
end
@@ -762,7 +778,7 @@ module Ci
let(:environment) { 'production staging' }
it 'raises error' do
- expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}")
+ expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}")
end
end
end
@@ -819,17 +835,16 @@ module Ci
it "doesn't create jobs that start with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
- except: nil,
stage: "test",
stage_idx: 1,
- name: :normal_job,
- only: nil,
+ name: "normal_job",
commands: "test",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
end
@@ -865,30 +880,28 @@ module Ci
it "is correctly supported for jobs" do
expect(subject.size).to eq(2)
expect(subject.first).to eq({
- except: nil,
stage: "build",
stage_idx: 0,
- name: :job1,
- only: nil,
+ name: "job1",
commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
expect(subject.second).to eq({
- except: nil,
stage: "build",
stage_idx: 0,
- name: :job2,
- only: nil,
+ name: "job2",
commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
allow_failure: false,
environment: nil,
+ yaml_variables: []
})
end
end
@@ -956,7 +969,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
@@ -970,7 +983,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
end
it "returns errors if after_script parameter is invalid" do
@@ -984,7 +997,7 @@ EOT
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
end
it "returns errors if image parameter is invalid" do
@@ -998,21 +1011,21 @@ EOT
config = YAML.dump({ '' => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
end
it "returns errors if job name is non-string" do
config = YAML.dump({ 10 => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
end
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string")
end
it "returns errors if services parameter is not an array" do
@@ -1033,49 +1046,56 @@ EOT
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings")
end
- it "returns errors if there are unknown parameters" do
+ it "returns error if job configuration is invalid" do
config = YAML.dump({ extra: "bundle update" })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ 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" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings")
end
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
+ end
+
+ it "returns errors if there are no visible jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
end
it "returns errors if job stage is not a pre-defined stage" do
@@ -1124,49 +1144,49 @@ EOT
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
end
it "returns errors if job artifacts:when is not an a predefined value" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
end
it "returns errors if job artifacts:expire_in is not an a string" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:expire_in is not an a valid duration" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
end
it "returns errors if cache:untracked is not an array of strings" do
@@ -1194,28 +1214,28 @@ EOT
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
- end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings")
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
end
diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb
index 4d8cb787dde..bbacdc67ebd 100644
--- a/spec/lib/container_registry/blob_spec.rb
+++ b/spec/lib/container_registry/blob_spec.rb
@@ -9,8 +9,9 @@ describe ContainerRegistry::Blob do
'size' => 1000
}
end
+ let(:token) { 'authorization-token' }
- let(:registry) { ContainerRegistry::Registry.new('http://example.com') }
+ let(:registry) { ContainerRegistry::Registry.new('http://example.com', token: token) }
let(:repository) { registry.repository('group/test') }
let(:blob) { repository.blob(config) }
@@ -58,4 +59,53 @@ describe ContainerRegistry::Blob do
it { is_expected.to be_truthy }
end
+
+ context '#data' do
+ let(:data) { '{"key":"value"}' }
+
+ subject { blob.data }
+
+ context 'when locally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: data)
+ end
+
+ it { is_expected.to eq(data) }
+ end
+
+ context 'when externally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:0123456789012345').
+ with(headers: { 'Authorization' => "bearer #{token}" }).
+ to_return(
+ status: 307,
+ headers: { 'Location' => location })
+ end
+
+ context 'for a valid address' do
+ let(:location) { 'http://external.com/blob/file' }
+
+ before do
+ stub_request(:get, location).
+ with(headers: { 'Authorization' => nil }).
+ to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: data)
+ end
+
+ it { is_expected.to eq(data) }
+ end
+
+ context 'for invalid file' do
+ let(:location) { 'file:///etc/passwd' }
+
+ it { expect{ subject }.to raise_error(ArgumentError, 'invalid address') }
+ end
+ end
+ end
end
diff --git a/spec/lib/container_registry/tag_spec.rb b/spec/lib/container_registry/tag_spec.rb
index c7324c2bf77..c5e31ae82b6 100644
--- a/spec/lib/container_registry/tag_spec.rb
+++ b/spec/lib/container_registry/tag_spec.rb
@@ -77,24 +77,47 @@ describe ContainerRegistry::Tag do
end
context 'config processing' do
- before do
- stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
- with(headers: { 'Accept' => 'application/octet-stream' }).
- to_return(
- status: 200,
- body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
- end
+ shared_examples 'a processable' do
+ context '#config' do
+ subject { tag.config }
- context '#config' do
- subject { tag.config }
+ it { is_expected.not_to be_nil }
+ end
+
+ context '#created_at' do
+ subject { tag.created_at }
- it { is_expected.not_to be_nil }
+ it { is_expected.not_to be_nil }
+ end
end
- context '#created_at' do
- subject { tag.created_at }
+ context 'when locally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ it_behaves_like 'a processable'
+ end
- it { is_expected.not_to be_nil }
+ context 'when externally stored' do
+ before do
+ stub_request(:get, 'http://example.com/v2/group/test/blobs/sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac').
+ with(headers: { 'Accept' => 'application/octet-stream' }).
+ to_return(
+ status: 307,
+ headers: { 'Location' => 'http://external.com/blob/file' })
+
+ stub_request(:get, 'http://external.com/blob/file').
+ to_return(
+ status: 200,
+ body: File.read(Rails.root + 'spec/fixtures/container_registry/config_blob.json'))
+ end
+
+ it_behaves_like 'a processable'
end
end
end
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index 309a88151cf..8f51474476d 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -5,7 +5,7 @@ describe DisableEmailInterceptor, lib: true do
Mail.register_interceptor(DisableEmailInterceptor)
end
- it 'should not send emails' do
+ it 'does not send emails' do
allow(Gitlab.config.gitlab).to receive(:email_enabled).and_return(false)
expect { deliver_mail }.not_to change(ActionMailer::Base.deliveries, :count)
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 566035c60d0..e10c1f5c547 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -25,18 +25,40 @@ describe ExtractsPath, lib: true do
@project = create(:project)
end
- it "log tree path should have no escape sequences" do
+ it "log tree path has no escape sequences" do
assign_ref_vars
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
- context 'escaped sequences in ref' do
- let(:ref) { "improve%2Fawesome" }
+ context 'ref contains %20' do
+ let(:ref) { 'foo%20bar' }
- it "id should have no escape sequences" do
+ it 'is not converted to a space in @id' do
+ @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+
+ assign_ref_vars
+
+ expect(@id).to start_with('foo%20bar/')
+ end
+ end
+
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+ it 'is not converted to %20 in @path' do
assign_ref_vars
- expect(@ref).to eq('improve/awesome')
- expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
+
+ expect(@path).to eq(params[:path])
+ end
+ end
+
+ context 'subclass overrides get_id' do
+ it 'uses ref returned by get_id' do
+ allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
+
+ assign_ref_vars
+
+ expect(@id).to eq(get_id)
end
end
end
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
deleted file mode 100644
index 88a71528867..00000000000
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AkismetHelper, type: :helper do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
- allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
- end
-
- describe '#check_for_spam?' do
- it 'returns true for non-member' do
- expect(helper.check_for_spam?(project, user)).to eq(true)
- end
-
- it 'returns false for member' do
- project.team << [user, :guest]
- expect(helper.check_for_spam?(project, user)).to eq(false)
- end
- end
-
- describe '#is_spam?' do
- it 'returns true for spam' do
- environment = {
- 'action_dispatch.remote_ip' => '127.0.0.1',
- 'HTTP_USER_AGENT' => 'Test User Agent'
- }
-
- allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
- expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
- end
- end
-end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 32ca8239845..4aba783dc33 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -8,7 +8,7 @@ module Gitlab
let(:html) { 'H<sub>2</sub>O' }
context "without project" do
- it "should convert the input using Asciidoctor and default options" do
+ it "converts the input using Asciidoctor and default options" do
expected_asciidoc_opts = {
safe: :secure,
backend: :html5,
@@ -24,7 +24,7 @@ module Gitlab
context "with asciidoc_opts" do
let(:asciidoc_opts) { { safe: :safe, attributes: ['foo'] } }
- it "should merge the options with default ones" do
+ it "merges the options with default ones" do
expected_asciidoc_opts = {
safe: :safe,
backend: :html5,
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 7bec1367156..b0772cad312 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -51,24 +51,24 @@ describe Gitlab::Auth, lib: true do
let(:username) { 'John' } # username isn't lowercase, test this
let(:password) { 'my-secret' }
- it "should find user by valid login/password" do
+ it "finds user by valid login/password" do
expect( gl_auth.find_with_user_password(username, password) ).to eql user
end
- it 'should find user by valid email/password with case-insensitive email' do
+ it 'finds user by valid email/password with case-insensitive email' do
expect(gl_auth.find_with_user_password(user.email.upcase, password)).to eql user
end
- it 'should find user by valid username/password with case-insensitive username' do
+ it 'finds user by valid username/password with case-insensitive username' do
expect(gl_auth.find_with_user_password(username.upcase, password)).to eql user
end
- it "should not find user with invalid password" do
+ it "does not find user with invalid password" do
password = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
- it "should not find user with invalid login" do
+ it "does not find user with invalid login" do
user = 'wrong'
expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
end
diff --git a/spec/lib/gitlab/badge/build/metadata_spec.rb b/spec/lib/gitlab/badge/build/metadata_spec.rb
new file mode 100644
index 00000000000..d678e522721
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/metadata_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Build::Metadata do
+ let(:badge) { double(project: create(:project), ref: 'feature') }
+ let(:metadata) { described_class.new(badge) }
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns build status title' do
+ expect(metadata.title).to eq 'build status'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/build.svg'
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
new file mode 100644
index 00000000000..38eebb2a176
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Status do
+ let(:project) { create(:project) }
+ let(:sha) { project.commit.sha }
+ let(:branch) { 'master' }
+ let(:badge) { described_class.new(project, branch) }
+
+ describe '#entity' do
+ it 'always says build' do
+ expect(badge.entity).to eq 'build'
+ end
+ end
+
+ describe '#template' do
+ it 'returns badge template' do
+ expect(badge.template.key_text).to eq 'build'
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns badge metadata' do
+ expect(badge.metadata.image_url)
+ .to include 'badges/master/build.svg'
+ end
+ end
+
+ context 'build exists' do
+ let!(:build) { create_build(project, sha, branch) }
+
+ context 'build success' do
+ before { build.success! }
+
+ describe '#status' do
+ it 'is successful' do
+ expect(badge.status).to eq 'success'
+ end
+ end
+ end
+
+ context 'build failed' do
+ before { build.drop! }
+
+ describe '#status' do
+ it 'failed' do
+ expect(badge.status).to eq 'failed'
+ end
+ end
+ end
+
+ context 'when outdated pipeline for given ref exists' do
+ before do
+ build.success!
+
+ old_build = create_build(project, '11eeffdd', branch)
+ old_build.drop!
+ end
+
+ it 'does not take outdated pipeline into account' do
+ expect(badge.status).to eq 'success'
+ end
+ end
+
+ context 'when multiple pipelines exist for given sha' do
+ before do
+ build.drop!
+
+ new_build = create_build(project, sha, branch)
+ new_build.success!
+ end
+
+ it 'reports the compound status' do
+ expect(badge.status).to eq 'failed'
+ end
+ end
+ end
+
+ context 'build does not exist' do
+ describe '#status' do
+ it 'is unknown' do
+ expect(badge.status).to eq 'unknown'
+ end
+ end
+ end
+
+ def create_build(project, sha, branch)
+ pipeline = create(:ci_empty_pipeline,
+ project: project,
+ sha: sha,
+ ref: branch)
+
+ create(:ci_build, pipeline: pipeline, stage: 'notify')
+ end
+end
diff --git a/spec/lib/gitlab/badge/build/template_spec.rb b/spec/lib/gitlab/badge/build/template_spec.rb
new file mode 100644
index 00000000000..a7e21fb8bb1
--- /dev/null
+++ b/spec/lib/gitlab/badge/build/template_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Build::Template do
+ let(:badge) { double(entity: 'build', status: 'success') }
+ let(:template) { described_class.new(badge) }
+
+ describe '#key_text' do
+ it 'is always says build' do
+ expect(template.key_text).to eq 'build'
+ end
+ end
+
+ describe '#value_text' do
+ it 'is status value' do
+ expect(template.value_text).to eq 'success'
+ end
+ end
+
+ describe 'widths and text anchors' do
+ it 'has fixed width and text anchors' do
+ expect(template.width).to eq 92
+ expect(template.key_width).to eq 38
+ expect(template.value_width).to eq 54
+ expect(template.key_text_anchor).to eq 19
+ expect(template.value_text_anchor).to eq 65
+ end
+ end
+
+ describe '#key_color' do
+ it 'is always the same' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when status is success' do
+ it 'has expected color' do
+ expect(template.value_color).to eq '#4c1'
+ end
+ end
+
+ context 'when status is failed' do
+ before do
+ allow(badge).to receive(:status).and_return('failed')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+
+ context 'when status is running' do
+ before do
+ allow(badge).to receive(:status).and_return('running')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
+ context 'when status is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return('unknown')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+
+ context 'when status does not match any known statuses' do
+ before do
+ allow(badge).to receive(:status).and_return('invalid')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb
deleted file mode 100644
index 2034445a197..00000000000
--- a/spec/lib/gitlab/badge/build_spec.rb
+++ /dev/null
@@ -1,123 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Badge::Build do
- let(:project) { create(:project) }
- let(:sha) { project.commit.sha }
- let(:branch) { 'master' }
- let(:badge) { described_class.new(project, branch) }
-
- describe '#type' do
- subject { badge.type }
- it { is_expected.to eq 'image/svg+xml' }
- end
-
- describe '#to_html' do
- let(:html) { Nokogiri::HTML.parse(badge.to_html) }
- let(:a_href) { html.at('a') }
-
- it 'points to link' do
- expect(a_href[:href]).to eq badge.link_url
- end
-
- it 'contains clickable image' do
- expect(a_href.children.first.name).to eq 'img'
- end
- end
-
- describe '#to_markdown' do
- subject { badge.to_markdown }
-
- it { is_expected.to include badge.image_url }
- it { is_expected.to include badge.link_url }
- end
-
- describe '#image_url' do
- subject { badge.image_url }
- it { is_expected.to include "badges/#{branch}/build.svg" }
- end
-
- describe '#link_url' do
- subject { badge.link_url }
- it { is_expected.to include "commits/#{branch}" }
- end
-
- context 'build exists' do
- let!(:build) { create_build(project, sha, branch) }
-
- context 'build success' do
- before { build.success! }
-
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-success' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains information about success' do
- expect(status_node(data, 'success')).to be_truthy
- end
- end
- end
-
- context 'build failed' do
- before { build.drop! }
-
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-failed' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains information about failure' do
- expect(status_node(data, 'failed')).to be_truthy
- end
- end
- end
- end
-
- context 'build does not exist' do
- describe '#to_s' do
- subject { badge.to_s }
- it { is_expected.to eq 'build-unknown' }
- end
-
- describe '#data' do
- let(:data) { badge.data }
-
- it 'contains infromation about unknown build' do
- expect(status_node(data, 'unknown')).to be_truthy
- end
- end
- end
-
- context 'when outdated pipeline for given ref exists' do
- before do
- build = create_build(project, sha, branch)
- build.success!
-
- old_build = create_build(project, '11eeffdd', branch)
- old_build.drop!
- end
-
- it 'does not take outdated pipeline into account' do
- expect(badge.to_s).to eq 'build-success'
- end
- end
-
- def create_build(project, sha, branch)
- pipeline = create(:ci_pipeline, project: project,
- sha: sha,
- ref: branch)
-
- create(:ci_build, pipeline: pipeline)
- end
-
- def status_node(data, status)
- xml = Nokogiri::XML.parse(data)
- xml.at(%Q{text:contains("#{status}")})
- end
-end
diff --git a/spec/lib/gitlab/badge/coverage/metadata_spec.rb b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
new file mode 100644
index 00000000000..74eaf7eaf8b
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/metadata_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+require 'lib/gitlab/badge/shared/metadata'
+
+describe Gitlab::Badge::Coverage::Metadata do
+ let(:badge) do
+ double(project: create(:project), ref: 'feature', job: 'test')
+ end
+
+ let(:metadata) { described_class.new(badge) }
+
+ it_behaves_like 'badge metadata'
+
+ describe '#title' do
+ it 'returns coverage report title' do
+ expect(metadata.title).to eq 'coverage report'
+ end
+ end
+
+ describe '#image_url' do
+ it 'returns valid url' do
+ expect(metadata.image_url).to include 'badges/feature/coverage.svg'
+ end
+ end
+
+ describe '#link_url' do
+ it 'returns valid link' do
+ expect(metadata.link_url).to include 'commits/feature'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
new file mode 100644
index 00000000000..ab0cce6e091
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Report do
+ let(:project) { create(:project) }
+ let(:job_name) { nil }
+
+ let(:badge) do
+ described_class.new(project, 'master', job_name)
+ end
+
+ describe '#entity' do
+ it 'describes a coverage' do
+ expect(badge.entity).to eq 'coverage'
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns correct metadata' do
+ expect(badge.metadata.image_url).to include 'coverage.svg'
+ end
+ end
+
+ describe '#template' do
+ it 'returns correct template' do
+ expect(badge.template.key_text).to eq 'coverage'
+ end
+ end
+
+ shared_examples 'unknown coverage report' do
+ context 'particular job specified' do
+ let(:job_name) { '' }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+
+ context 'particular job not specified' do
+ let(:job_name) { nil }
+
+ it 'returns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+
+ context 'when latest successful pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
+ create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+ end
+
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+ end
+ end
+
+ context 'when particular job specified' do
+ let(:job_name) { 'first' }
+
+ it 'returns coverage for the particular job' do
+ expect(badge.status).to eq 40
+ end
+ end
+
+ context 'when particular job not specified' do
+ let(:job_name) { '' }
+
+ it 'returns arithemetic mean for the pipeline' do
+ expect(badge.status).to eq 50
+ end
+ end
+ end
+
+ context 'when only failed pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
+ end
+ end
+
+ it_behaves_like 'unknown coverage report'
+
+ context 'particular job specified' do
+ let(:job_name) { 'nonexistent' }
+
+ it 'retruns nil' do
+ expect(badge.status).to be_nil
+ end
+ end
+ end
+
+ context 'pipeline does not exist' do
+ it_behaves_like 'unknown coverage report'
+ end
+
+ def create_pipeline
+ opts = { project: project, sha: project.commit.id, ref: 'master' }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
new file mode 100644
index 00000000000..383bae6e087
--- /dev/null
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe Gitlab::Badge::Coverage::Template do
+ let(:badge) { double(entity: 'coverage', status: 90) }
+ let(:template) { described_class.new(badge) }
+
+ describe '#key_text' do
+ it 'is always says coverage' do
+ expect(template.key_text).to eq 'coverage'
+ end
+ end
+
+ describe '#value_text' do
+ context 'when coverage is known' do
+ it 'returns coverage percentage' do
+ expect(template.value_text).to eq '90%'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns string that says coverage is unknown' do
+ expect(template.value_text).to eq 'unknown'
+ end
+ end
+ end
+
+ describe '#key_width' do
+ it 'has a fixed key width' do
+ expect(template.key_width).to eq 62
+ end
+ end
+
+ describe '#value_width' do
+ context 'when coverage is known' do
+ it 'is narrower when coverage is known' do
+ expect(template.value_width).to eq 36
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is wider when coverage is unknown to fit text' do
+ expect(template.value_width).to eq 58
+ end
+ end
+ end
+
+ describe '#key_color' do
+ it 'always has the same color' do
+ expect(template.key_color).to eq '#555'
+ end
+ end
+
+ describe '#value_color' do
+ context 'when coverage is good' do
+ before do
+ allow(badge).to receive(:status).and_return(98)
+ end
+
+ it 'is green' do
+ expect(template.value_color).to eq '#4c1'
+ end
+ end
+
+ context 'when coverage is acceptable' do
+ before do
+ allow(badge).to receive(:status).and_return(90)
+ end
+
+ it 'is green-orange' do
+ expect(template.value_color).to eq '#a3c51c'
+ end
+ end
+
+ context 'when coverage is medium' do
+ before do
+ allow(badge).to receive(:status).and_return(75)
+ end
+
+ it 'is orange-yellow' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
+ context 'when coverage is low' do
+ before do
+ allow(badge).to receive(:status).and_return(50)
+ end
+
+ it 'is red' do
+ expect(template.value_color).to eq '#e05d44'
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'is grey' do
+ expect(template.value_color).to eq '#9f9f9f'
+ end
+ end
+ end
+
+ describe '#width' do
+ context 'when coverage is known' do
+ it 'returns the key width plus value width' do
+ expect(template.width).to eq 98
+ end
+ end
+
+ context 'when coverage is unknown' do
+ before do
+ allow(badge).to receive(:status).and_return(nil)
+ end
+
+ it 'returns key width plus wider value width' do
+ expect(template.width).to eq 120
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/badge/shared/metadata.rb b/spec/lib/gitlab/badge/shared/metadata.rb
new file mode 100644
index 00000000000..0cf18514251
--- /dev/null
+++ b/spec/lib/gitlab/badge/shared/metadata.rb
@@ -0,0 +1,21 @@
+shared_examples 'badge metadata' do
+ describe '#to_html' do
+ let(:html) { Nokogiri::HTML.parse(metadata.to_html) }
+ let(:a_href) { html.at('a') }
+
+ it 'points to link' do
+ expect(a_href[:href]).to eq metadata.link_url
+ end
+
+ it 'contains clickable image' do
+ expect(a_href.children.first.name).to eq 'img'
+ end
+ end
+
+ describe '#to_markdown' do
+ subject { metadata.to_markdown }
+
+ it { is_expected.to include metadata.image_url }
+ it { is_expected.to include metadata.link_url }
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
index 760d66a1488..7543c29bcc4 100644
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -54,12 +54,12 @@ describe Gitlab::BitbucketImport::Client, lib: true do
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" } })
- project.import_url = "ssh://git@bitbucket.org/test/test.git"
expect { described_class.from_project(project) }.not_to raise_error
end
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb
deleted file mode 100644
index 38be9448794..00000000000
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'spec_helper'
-
-describe 'Gitlab::BuildDataBuilder' do
- let(:build) { create(:ci_build) }
-
- describe :build do
- let(:data) do
- Gitlab::BuildDataBuilder.build(build)
- end
-
- it { expect(data).to be_a(Hash) }
- it { expect(data[:ref]).to eq(build.ref) }
- it { expect(data[:sha]).to eq(build.sha) }
- it { expect(data[:tag]).to eq(build.tag) }
- it { expect(data[:build_id]).to eq(build.id) }
- it { expect(data[:build_status]).to eq(build.status) }
- it { expect(data[:build_allow_failure]).to eq(false) }
- it { expect(data[:project_id]).to eq(build.project.id) }
- it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
- end
-end
diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb
new file mode 100644
index 00000000000..69d86144e32
--- /dev/null
+++ b/spec/lib/gitlab/changes_list_spec.rb
@@ -0,0 +1,30 @@
+require "spec_helper"
+
+describe Gitlab::ChangesList do
+ let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" }
+ let(:invalid_changes) { 1 }
+
+ context 'when changes is a valid string' do
+ let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) }
+
+ it 'splits elements by newline character' do
+ expect(changes_list).to contain_exactly({
+ oldrev: "000000",
+ newrev: "570e7b2",
+ ref: "refs/heads/my_branch"
+ }, {
+ oldrev: "d14d6c",
+ newrev: "6fd24d",
+ ref: "refs/heads/master"
+ })
+ end
+
+ it 'behaves like a list' do
+ expect(changes_list.first).to eq({
+ oldrev: "000000",
+ newrev: "570e7b2",
+ ref: "refs/heads/my_branch"
+ })
+ end
+ end
+end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
new file mode 100644
index 00000000000..39069b49978
--- /dev/null
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::ChangeAccess, lib: true do
+ describe '#exec' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+ ref: 'refs/heads/master'
+ }
+ end
+
+ subject { described_class.new(changes, project: project, user_access: user_access).exec }
+
+ before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
+
+ context 'without failed checks' do
+ it "doesn't return any error" do
+ expect(subject.status).to be(true)
+ end
+ end
+
+ context 'when the user is not allowed to push code' do
+ it 'returns an error' do
+ expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to push code to this project.')
+ end
+ end
+
+ context 'tags check' do
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
+ ref: 'refs/tags/v1.0.0'
+ }
+ end
+
+ it 'returns an error if the user is not allowed to update tags' do
+ expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
+ end
+ end
+
+ context 'protected branches check' do
+ before do
+ allow(project).to receive(:protected_branch?).with('master').and_return(true)
+ end
+
+ it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
+ expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true)
+ expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.')
+ end
+
+ it 'returns an error if the user is not allowed to merge to protected branches' do
+ expect_any_instance_of(Gitlab::Checks::MatchingMergeRequest).to receive(:match?).and_return(true)
+ expect(user_access).to receive(:can_merge_to_branch?).and_return(false)
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to merge code into protected branches on this project.')
+ end
+
+ it 'returns an error if the user is not allowed to push to protected branches' do
+ expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to push code to protected branches on this project.')
+ end
+
+ context 'branch deletion' do
+ let(:changes) do
+ {
+ oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
+ newrev: '0000000000000000000000000000000000000000',
+ ref: 'refs/heads/master'
+ }
+ end
+
+ it 'returns an error if the user is not allowed to delete protected branches' do
+ expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false)
+
+ expect(subject.status).to be(false)
+ expect(subject.message).to eq('You are not allowed to delete protected branches from this project.')
+ end
+ 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
new file mode 100644
index 00000000000..c09a0a9c793
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb
@@ -0,0 +1,45 @@
+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
new file mode 100644
index 00000000000..24d9daafd88
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/attributable_spec.rb
@@ -0,0 +1,43 @@
+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/commands_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb
new file mode 100644
index 00000000000..e373c40706f
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/commands_spec.rb
@@ -0,0 +1,49 @@
+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/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
index 91ddef7bfbf..d26185ba585 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Factory do
describe '#create!' do
- let(:factory) { described_class.new(entry_class) }
- let(:entry_class) { Gitlab::Ci::Config::Node::Script }
+ let(:factory) { described_class.new(node) }
+ let(:node) { Gitlab::Ci::Config::Node::Script }
- context 'when setting up a value' do
+ context 'when setting a concrete value' do
it 'creates entry with valid value' do
entry = factory
- .with(value: ['ls', 'pwd'])
+ .value(['ls', 'pwd'])
.create!
expect(entry.value).to eq ['ls', 'pwd']
@@ -17,7 +17,7 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when setting description' do
it 'creates entry with description' do
entry = factory
- .with(value: ['ls', 'pwd'])
+ .value(['ls', 'pwd'])
.with(description: 'test description')
.create!
@@ -29,7 +29,8 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when setting key' do
it 'creates entry with custom key' do
entry = factory
- .with(value: ['ls', 'pwd'], key: 'test key')
+ .value(['ls', 'pwd'])
+ .with(key: 'test key')
.create!
expect(entry.key).to eq 'test key'
@@ -37,19 +38,20 @@ describe Gitlab::Ci::Config::Node::Factory do
end
context 'when setting a parent' do
- let(:parent) { Object.new }
+ let(:object) { Object.new }
it 'creates entry with valid parent' do
entry = factory
- .with(value: 'ls', parent: parent)
+ .value('ls')
+ .with(parent: object)
.create!
- expect(entry.parent).to eq parent
+ expect(entry.parent).to eq object
end
end
end
- context 'when not setting up a value' do
+ context 'when not setting a value' do
it 'raises error' do
expect { factory.create! }.to raise_error(
Gitlab::Ci::Config::Node::Factory::InvalidFactory
@@ -60,11 +62,25 @@ describe Gitlab::Ci::Config::Node::Factory do
context 'when creating entry with nil value' do
it 'creates an undefined entry' do
entry = factory
- .with(value: nil)
+ .value(nil)
.create!
expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
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
index c87c9e97bc8..2f87d270b36 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -22,38 +22,40 @@ describe Gitlab::Ci::Config::Node::Global do
variables: { VAR: 'value' },
after_script: ['make clean'],
stages: ['build', 'pages'],
- cache: { key: 'k', untracked: true, paths: ['public/'] } }
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ rspec: { script: %w[rspec ls] },
+ spinach: { script: 'spinach' } }
end
describe '#process!' do
before { global.process! }
it 'creates nodes hash' do
- expect(global.nodes).to be_an Array
+ expect(global.descendants).to be_an Array
end
it 'creates node object for each entry' do
- expect(global.nodes.count).to eq 8
+ expect(global.descendants.count).to eq 8
end
it 'creates node object using valid class' do
- expect(global.nodes.first)
+ expect(global.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Script
- expect(global.nodes.second)
+ expect(global.descendants.second)
.to be_an_instance_of Gitlab::Ci::Config::Node::Image
end
it 'sets correct description for nodes' do
- expect(global.nodes.first.description)
+ expect(global.descendants.first.description)
.to eq 'Script that will be executed before each job.'
- expect(global.nodes.second.description)
+ expect(global.descendants.second.description)
.to eq 'Docker image that will be used to execute jobs.'
end
- end
- describe '#leaf?' do
- it 'is not leaf' do
- expect(global).not_to be_leaf
+ describe '#leaf?' do
+ it 'is not leaf' do
+ expect(global).not_to be_leaf
+ end
end
end
@@ -63,6 +65,12 @@ describe Gitlab::Ci::Config::Node::Global 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 processed' do
@@ -106,7 +114,10 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when deprecated types key defined' do
- let(:hash) { { types: ['test', 'deploy'] } }
+ 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]
@@ -120,20 +131,33 @@ describe Gitlab::Ci::Config::Node::Global do
.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],
+ stage: 'test' },
+ spinach: { name: :spinach,
+ script: %w[spinach],
+ stage: 'test' }
+ )
+ end
+ end
end
end
context 'when most of entires not defined' do
- let(:hash) { { cache: { key: 'a' }, rspec: {} } }
+ let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
before { global.process! }
describe '#nodes' do
it 'instantizes all nodes' do
- expect(global.nodes.count).to eq 8
+ expect(global.descendants.count).to eq 8
end
it 'contains undefined nodes' do
- expect(global.nodes.first)
+ expect(global.descendants.first)
.to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
end
end
@@ -164,7 +188,7 @@ describe Gitlab::Ci::Config::Node::Global do
# details.
#
context 'when entires specified but not defined' do
- let(:hash) { { variables: nil } }
+ let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
before { global.process! }
describe '#variables' do
@@ -196,10 +220,8 @@ describe Gitlab::Ci::Config::Node::Global do
end
describe '#before_script' do
- it 'raises error' do
- expect { global.before_script }.to raise_error(
- Gitlab::Ci::Config::Node::Entry::InvalidError
- )
+ it 'returns nil' do
+ expect(global.before_script).to be_nil
end
end
end
@@ -220,9 +242,9 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- describe '#defined?' do
+ describe '#specified?' do
it 'is concrete entry that is defined' do
- expect(global.defined?).to be true
+ expect(global.specified?).to be true
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
new file mode 100644
index 00000000000..cc44e2cc054
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::HiddenJob do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ context 'when entry config value is correct' do
+ let(:config) { { image: 'ruby:2.2' } }
+
+ describe '#value' do
+ it 'returns key value' do
+ expect(entry.value).to eq(image: 'ruby:2.2')
+ 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 'incorrect config value type' do
+ let(:config) { ['incorrect'] }
+
+ describe '#errors' do
+ it 'saves errors' do
+ expect(entry.errors)
+ .to include 'hidden 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
+ 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/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
new file mode 100644
index 00000000000..1484fb60dd8
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Job do
+ let(:entry) { described_class.new(config, name: :rspec) }
+
+ before { entry.process! }
+
+ describe 'validations' do
+ 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 '#value' do
+ 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],
+ stage: 'test',
+ after_script: %w[cleanup])
+ end
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ expect(entry).to be_relevant
+ 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
new file mode 100644
index 00000000000..b8d9c70479c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Jobs do
+ let(:entry) { described_class.new(config) }
+
+ describe 'validations' do
+ before { entry.process! }
+
+ 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 processed' do
+ before { entry.process! }
+
+ 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],
+ stage: 'test' },
+ spinach: { name: :spinach,
+ script: %w[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::HiddenJob)
+ 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/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
new file mode 100644
index 00000000000..1ab5478dcfa
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/null_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Null do
+ let(:null) { described_class.new(nil) }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(null).to be_leaf
+ end
+ end
+
+ describe '#valid?' do
+ it 'is always valid' do
+ expect(null).to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'is does not contain errors' do
+ expect(null.errors).to be_empty
+ end
+ end
+
+ describe '#value' do
+ it 'returns nil' do
+ expect(null.value).to eq nil
+ end
+ end
+
+ describe '#relevant?' do
+ it 'is not relevant' do
+ expect(null.relevant?).to eq false
+ end
+ end
+
+ describe '#specified?' do
+ it 'is not defined' do
+ expect(null.specified?).to eq false
+ 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
new file mode 100644
index 00000000000..fb9ec70762a
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb
@@ -0,0 +1,38 @@
+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/trigger_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb
new file mode 100644
index 00000000000..a4a3e36754e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/trigger_spec.rb
@@ -0,0 +1,56 @@
+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
index 0c6608d906d..2d43e1c1a9d 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -2,39 +2,31 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
let(:undefined) { described_class.new(entry) }
- let(:entry) { Class.new }
-
- describe '#leaf?' do
- it 'is leaf node' do
- expect(undefined).to be_leaf
- end
- end
+ let(:entry) { spy('Entry') }
describe '#valid?' do
- it 'is always valid' do
- expect(undefined).to be_valid
+ it 'delegates method to entry' do
+ expect(undefined.valid).to eq entry
end
end
describe '#errors' do
- it 'is does not contain errors' do
- expect(undefined.errors).to be_empty
+ it 'delegates method to entry' do
+ expect(undefined.errors).to eq entry
end
end
describe '#value' do
- before do
- allow(entry).to receive(:default).and_return('some value')
- end
-
- it 'returns default value for entry' do
- expect(undefined.value).to eq 'some value'
+ it 'delegates method to entry' do
+ expect(undefined.value).to eq entry
end
end
- describe '#undefined?' do
- it 'is not a defined entry' do
- expect(undefined.defined?).to be false
+ describe '#specified?' do
+ it 'is always false' do
+ allow(entry).to receive(:specified?).and_return(true)
+
+ expect(undefined.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
index 10cd01afcd1..64b77fd6e03 100644
--- a/spec/lib/gitlab/ci/config/node/validatable_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/validatable_spec.rb
@@ -23,6 +23,10 @@ describe Gitlab::Ci::Config::Node::Validatable do
.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 }
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index e9b8ce6b5bb..de3f64249a2 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -3,10 +3,12 @@ require 'spec_helper'
describe Gitlab::ClosingIssueExtractor, lib: true do
let(:project) { create(:project) }
let(:project2) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, project.creator).execute }
let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
let(:cross_reference) { issue2.to_reference(project) }
+ let(:fork_cross_reference) { issue.to_reference(forked_project) }
subject { described_class.new(project, project.creator) }
@@ -278,6 +280,15 @@ describe Gitlab::ClosingIssueExtractor, lib: true do
end
end
+ context "with a cross-project fork reference" do
+ subject { described_class.new(forked_project, forked_project.creator) }
+
+ it do
+ message = "Closes #{fork_cross_reference}"
+ expect(subject.closed_by_message(message)).to be_empty
+ end
+ end
+
context "with an invalid URL" do
it do
message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}"
diff --git a/spec/lib/gitlab/conflict/file_collection_spec.rb b/spec/lib/gitlab/conflict/file_collection_spec.rb
new file mode 100644
index 00000000000..39d892c18c0
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::FileCollection, lib: true do
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start') }
+ let(:file_collection) { Gitlab::Conflict::FileCollection.new(merge_request) }
+
+ describe '#files' do
+ it 'returns an array of Conflict::Files' do
+ expect(file_collection.files).to all(be_an_instance_of(Gitlab::Conflict::File))
+ end
+ end
+
+ describe '#default_commit_message' do
+ it 'matches the format of the git CLI commit message' do
+ expect(file_collection.default_commit_message).to eq(<<EOM.chomp)
+Merge branch 'conflict-start' into 'conflict-resolvable'
+
+# Conflicts:
+# files/ruby/popen.rb
+# files/ruby/regex.rb
+EOM
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb
new file mode 100644
index 00000000000..60020487061
--- /dev/null
+++ b/spec/lib/gitlab/conflict/file_spec.rb
@@ -0,0 +1,261 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::File, lib: true do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+ let(:rugged) { repository.rugged }
+ let(:their_commit) { rugged.branches['conflict-start'].target }
+ let(:our_commit) { rugged.branches['conflict-resolvable'].target }
+ let(:merge_request) { create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) }
+ let(:index) { rugged.merge_commits(our_commit, their_commit) }
+ let(:conflict) { index.conflicts.last }
+ let(:merge_file_result) { index.merge_file('files/ruby/regex.rb') }
+ let(:conflict_file) { Gitlab::Conflict::File.new(merge_file_result, conflict, merge_request: merge_request) }
+
+ describe '#resolve_lines' do
+ let(:section_keys) { conflict_file.sections.map { |section| section[:id] }.compact }
+
+ context 'when resolving everything to the same side' do
+ let(:resolution_hash) { section_keys.map { |key| [key, 'head'] }.to_h }
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+ let(:expected_lines) { conflict_file.lines.reject { |line| line.type == 'old' } }
+
+ it 'has the correct number of lines' do
+ expect(resolved_lines.length).to eq(expected_lines.length)
+ end
+
+ it 'has content matching the chosen lines' do
+ expect(resolved_lines.map(&:text)).to eq(expected_lines.map(&:text))
+ end
+ end
+
+ context 'with mixed resolutions' do
+ let(:resolution_hash) do
+ section_keys.map.with_index { |key, i| [key, i.even? ? 'head' : 'origin'] }.to_h
+ end
+
+ let(:resolved_lines) { conflict_file.resolve_lines(resolution_hash) }
+
+ it 'has the correct number of lines' do
+ file_lines = conflict_file.lines.reject { |line| line.type == 'new' }
+
+ expect(resolved_lines.length).to eq(file_lines.length)
+ end
+
+ it 'returns a file containing only the chosen parts of the resolved sections' do
+ expect(resolved_lines.chunk { |line| line.type || 'both' }.map(&:first)).
+ to eq(['both', 'new', 'both', 'old', 'both', 'new', 'both'])
+ end
+ end
+
+ it 'raises MissingResolution when passed a hash without resolutions for all sections' do
+ empty_hash = section_keys.map { |key| [key, nil] }.to_h
+ invalid_hash = section_keys.map { |key| [key, 'invalid'] }.to_h
+
+ expect { conflict_file.resolve_lines({}) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(empty_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+
+ expect { conflict_file.resolve_lines(invalid_hash) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+
+ describe '#highlight_lines!' do
+ def html_to_text(html)
+ CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html)).delete("\n")
+ end
+
+ it 'modifies the existing lines' do
+ expect { conflict_file.highlight_lines! }.to change { conflict_file.lines.map(&:instance_variables) }
+ end
+
+ it 'is called implicitly when rich_text is accessed on a line' do
+ expect(conflict_file).to receive(:highlight_lines!).once.and_call_original
+
+ conflict_file.lines.each(&:rich_text)
+ end
+
+ it 'sets the rich_text of the lines matching the text content' do
+ conflict_file.lines.each do |line|
+ expect(line.text).to eq(html_to_text(line.rich_text))
+ end
+ end
+ end
+
+ describe '#sections' do
+ it 'only inserts match lines when there is a gap between sections' do
+ conflict_file.sections.each_with_index do |section, i|
+ previous_line_number = 0
+ current_line_number = section[:lines].map(&:old_line).compact.min
+
+ if i > 0
+ previous_line_number = conflict_file.sections[i - 1][:lines].map(&:old_line).compact.last
+ end
+
+ if current_line_number == previous_line_number + 1
+ expect(section[:lines].first.type).not_to eq('match')
+ else
+ expect(section[:lines].first.type).to eq('match')
+ expect(section[:lines].first.text).to match(/\A@@ -#{current_line_number},\d+ \+\d+,\d+ @@ module Gitlab\Z/)
+ end
+ end
+ end
+
+ it 'sets conflict to false for sections with only unchanged lines' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match).to all(have_attributes(type: nil))
+ end
+ end
+
+ it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
+ conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
+ without_match = section[:lines].reject { |line| line.type == 'match' }
+
+ expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
+ end
+ end
+
+ it 'sets conflict to true for sections with only changed lines' do
+ conflict_file.sections.select { |section| section[:conflict] }.each do |section|
+ section[:lines].each do |line|
+ expect(line.type).to be_in(['new', 'old'])
+ end
+ end
+ end
+
+ it 'adds unique IDs to conflict sections, and not to other sections' do
+ section_ids = []
+
+ conflict_file.sections.each do |section|
+ if section[:conflict]
+ expect(section).to have_key(:id)
+ section_ids << section[:id]
+ else
+ expect(section).not_to have_key(:id)
+ end
+ end
+
+ expect(section_ids.uniq).to eq(section_ids)
+ end
+
+ context 'with an example file' do
+ let(:file) do
+ <<FILE
+ # Ensure there is no match line header here
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+end
+
+def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+end
+
+def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+end
+
+# Some extra lines
+# To force a match line
+# To be created
+
+def path_regexp
+ default_regexp
+end
+
+<<<<<<< files/ruby/regex.rb
+def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+end
+
+def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+end
+
+protected
+
+<<<<<<< files/ruby/regex.rb
+def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+end
+FILE
+ end
+
+ let(:conflict_file) { Gitlab::Conflict::File.new({ data: file }, conflict, merge_request: merge_request) }
+ let(:sections) { conflict_file.sections }
+
+ it 'sets the correct match line headers' do
+ expect(sections[0][:lines].first).to have_attributes(type: 'match', text: '@@ -3,14 +3,14 @@')
+ expect(sections[3][:lines].first).to have_attributes(type: 'match', text: '@@ -19,26 +19,26 @@ def path_regexp')
+ expect(sections[6][:lines].first).to have_attributes(type: 'match', text: '@@ -47,52 +47,52 @@ end')
+ end
+
+ it 'does not add match lines where they are not needed' do
+ expect(sections[1][:lines].first.type).not_to eq('match')
+ expect(sections[2][:lines].first.type).not_to eq('match')
+ expect(sections[4][:lines].first.type).not_to eq('match')
+ expect(sections[5][:lines].first.type).not_to eq('match')
+ expect(sections[7][:lines].first.type).not_to eq('match')
+ end
+
+ it 'creates context sections of the correct length' do
+ expect(sections[0][:lines].reject(&:type).length).to eq(3)
+ expect(sections[2][:lines].reject(&:type).length).to eq(3)
+ expect(sections[3][:lines].reject(&:type).length).to eq(3)
+ expect(sections[5][:lines].reject(&:type).length).to eq(3)
+ expect(sections[6][:lines].reject(&:type).length).to eq(3)
+ expect(sections[8][:lines].reject(&:type).length).to eq(1)
+ end
+ end
+ end
+
+ describe '#as_json' do
+ it 'includes the blob path for the file' do
+ expect(conflict_file.as_json[:blob_path]).
+ to eq("/#{project.namespace.to_param}/#{merge_request.project.to_param}/blob/#{our_commit.oid}/files/ruby/regex.rb")
+ end
+
+ it 'includes the blob icon for the file' do
+ expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
new file mode 100644
index 00000000000..a1d2ca1e272
--- /dev/null
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe Gitlab::Conflict::Parser, lib: true do
+ let(:parser) { Gitlab::Conflict::Parser.new }
+
+ describe '#parse' do
+ def parse_text(text)
+ parser.parse(text, our_path: 'README.md', their_path: 'README.md')
+ end
+
+ context 'when the file has valid conflicts' do
+ let(:text) do
+ <<CONFLICT
+module Gitlab
+ module Regexp
+ extend self
+
+ def username_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def project_name_regexp
+ /\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
+ end
+
+ def name_regexp
+ /\A[a-zA-Z0-9_\-\. ]*\z/
+=======
+ def project_name_regex
+ %r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
+ end
+
+ def name_regex
+ %r{\A[a-zA-Z0-9_\-\. ]*\z}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def path_regexp
+ default_regexp
+ end
+
+<<<<<<< files/ruby/regex.rb
+ def archive_formats_regexp
+ /(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+=======
+ def archive_formats_regex
+ %r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
+>>>>>>> files/ruby/regex.rb
+ end
+
+ def git_reference_regexp
+ # Valid git ref regexp, see:
+ # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
+ %r{
+ (?!
+ (?# doesn't begins with)
+ \/| (?# rule #6)
+ (?# doesn't contain)
+ .*(?:
+ [\/.]\.| (?# rule #1,3)
+ \/\/| (?# rule #6)
+ @\{| (?# rule #8)
+ \\ (?# rule #9)
+ )
+ )
+ [^\000-\040\177~^:?*\[]+ (?# rule #4-5)
+ (?# doesn't end with)
+ (?<!\.lock) (?# rule #1)
+ (?<![\/.]) (?# rule #6-7)
+ }x
+ end
+
+ protected
+
+<<<<<<< files/ruby/regex.rb
+ def default_regexp
+ /\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+=======
+ def default_regex
+ %r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
+>>>>>>> files/ruby/regex.rb
+ end
+ end
+end
+CONFLICT
+ end
+
+ let(:lines) do
+ parser.parse(text, our_path: 'files/ruby/regex.rb', their_path: 'files/ruby/regex.rb')
+ end
+
+ it 'sets our lines as new lines' do
+ expect(lines[8..13]).to all(have_attributes(type: 'new'))
+ expect(lines[26..27]).to all(have_attributes(type: 'new'))
+ expect(lines[56..57]).to all(have_attributes(type: 'new'))
+ end
+
+ it 'sets their lines as old lines' do
+ expect(lines[14..19]).to all(have_attributes(type: 'old'))
+ expect(lines[28..29]).to all(have_attributes(type: 'old'))
+ expect(lines[58..59]).to all(have_attributes(type: 'old'))
+ end
+
+ it 'sets non-conflicted lines as both' do
+ expect(lines[0..7]).to all(have_attributes(type: nil))
+ expect(lines[20..25]).to all(have_attributes(type: nil))
+ expect(lines[30..55]).to all(have_attributes(type: nil))
+ expect(lines[60..62]).to all(have_attributes(type: nil))
+ end
+
+ it 'sets consecutive line numbers for index, old_pos, and new_pos' do
+ old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
+ new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
+
+ expect(lines.map(&:index)).to eq(0.upto(62).to_a)
+ expect(old_line_numbers).to eq(1.upto(53).to_a)
+ expect(new_line_numbers).to eq(1.upto(53).to_a)
+ end
+ end
+
+ context 'when the file contents include conflict delimiters' do
+ it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
+ expect { parse_text('=======') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> README.md') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text('>>>>>>> some-other-path.md') }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
+ start_text = "<<<<<<< README.md\n"
+ end_text = "\n=======\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
+ start_text = "<<<<<<< README.md\n=======\n"
+ end_text = "\n>>>>>>> README.md"
+
+ expect { parse_text(start_text + '=======' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
+
+ it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
+ start_text = "<<<<<<< README.md\n=======\n"
+
+ expect { parse_text(start_text) }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
+ to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
+ end
+ end
+
+ context 'other file types' do
+ it 'raises UnmergeableFile when lines is blank, indicating a binary file' do
+ expect { parse_text('') }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+
+ expect { parse_text(nil) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnmergeableFile when the file is over 100 KB' do
+ expect { parse_text('a' * 102401) }.
+ to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
+ end
+
+ it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
new file mode 100644
index 00000000000..6c71e98066b
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Build do
+ let(:build) { create(:ci_build) }
+
+ describe '.build' do
+ let(:data) do
+ described_class.build(build)
+ end
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:ref]).to eq(build.ref) }
+ it { expect(data[:sha]).to eq(build.sha) }
+ it { expect(data[:tag]).to eq(build.tag) }
+ it { expect(data[:build_id]).to eq(build.id) }
+ it { expect(data[:build_status]).to eq(build.status) }
+ it { expect(data[:build_allow_failure]).to eq(false) }
+ it { expect(data[:project_id]).to eq(build.project.id) }
+ it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb
new file mode 100644
index 00000000000..9a4dec91e56
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/note_spec.rb
@@ -0,0 +1,131 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Note, lib: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:data) { described_class.build(note, user) }
+ let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
+
+ before(:each) do
+ expect(data).to have_key(:object_attributes)
+ expect(data[:object_attributes]).to have_key(:url)
+ expect(data[:object_attributes][:url])
+ .to eq(Gitlab::UrlBuilder.build(note))
+ expect(data[:object_kind]).to eq('note')
+ expect(data[:user]).to eq(user.hook_attrs)
+ end
+
+ describe 'When asking for a note on commit' do
+ let(:note) { create(:note_on_commit, project: project) }
+
+ it 'returns the note and commit-specific data' do
+ expect(data).to have_key(:commit)
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe 'When asking for a note on commit diff' do
+ let(:note) { create(:diff_note_on_commit, project: project) }
+
+ it 'returns the note and commit-specific data' do
+ expect(data).to have_key(:commit)
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe 'When asking for a note on issue' do
+ let(:issue) do
+ create(:issue, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let(:note) do
+ create(:note_on_issue, noteable: issue, project: project)
+ end
+
+ it 'returns the note and issue-specific data' do
+ expect(data).to have_key(:issue)
+ expect(data[:issue].except('updated_at'))
+ .to eq(issue.reload.hook_attrs.except('updated_at'))
+ expect(data[:issue]['updated_at'])
+ .to be > issue.hook_attrs['updated_at']
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe 'When asking for a note on merge request' do
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time,
+ updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project)
+ end
+
+ it 'returns the note and merge request data' do
+ expect(data).to have_key(:merge_request)
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at'])
+ .to be > merge_request.hook_attrs['updated_at']
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe 'When asking for a note on merge request diff' do
+ let(:merge_request) do
+ create(:merge_request, created_at: fixed_time, updated_at: fixed_time,
+ source_project: project)
+ end
+
+ let(:note) do
+ create(:diff_note_on_merge_request, noteable: merge_request,
+ project: project)
+ end
+
+ it 'returns the note and merge request diff data' do
+ expect(data).to have_key(:merge_request)
+ expect(data[:merge_request].except('updated_at'))
+ .to eq(merge_request.reload.hook_attrs.except('updated_at'))
+ expect(data[:merge_request]['updated_at'])
+ .to be > merge_request.hook_attrs['updated_at']
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe 'When asking for a note on project snippet' do
+ let!(:snippet) do
+ create(:project_snippet, created_at: fixed_time, updated_at: fixed_time,
+ project: project)
+ end
+
+ let!(:note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project)
+ end
+
+ it 'returns the note and project snippet data' do
+ expect(data).to have_key(:snippet)
+ expect(data[:snippet].except('updated_at'))
+ .to eq(snippet.reload.hook_attrs.except('updated_at'))
+ expect(data[:snippet]['updated_at'])
+ .to be > snippet.hook_attrs['updated_at']
+ end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
new file mode 100644
index 00000000000..a68f5943a6a
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Pipeline do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ status: 'success',
+ sha: project.commit.sha,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+
+ describe '.build' do
+ let(:data) { described_class.build(pipeline) }
+ let(:attributes) { data[:object_attributes] }
+ let(:build_data) { data[:builds].first }
+ let(:project_data) { data[:project] }
+
+ it { expect(attributes).to be_a(Hash) }
+ it { expect(attributes[:ref]).to eq(pipeline.ref) }
+ it { expect(attributes[:sha]).to eq(pipeline.sha) }
+ it { expect(attributes[:tag]).to eq(pipeline.tag) }
+ it { expect(attributes[:id]).to eq(pipeline.id) }
+ it { expect(attributes[:status]).to eq(pipeline.status) }
+
+ it { expect(build_data).to be_a(Hash) }
+ it { expect(build_data[:id]).to eq(build.id) }
+ it { expect(build_data[:status]).to eq(build.status) }
+
+ it { expect(project_data).to eq(project.hook_attrs(backward: false)) }
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
new file mode 100644
index 00000000000..b73434e8dd7
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Push, lib: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ describe '.build_sample' do
+ let(:data) { described_class.build_sample(project, user) }
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(data[:ref]).to eq('refs/heads/master') }
+ it { expect(data[:commits].size).to eq(3) }
+ it { expect(data[:total_commits_count]).to eq(3) }
+ it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
+ it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
+ it { expect(data[:commits].first[:removed]).to eq([]) }
+
+ include_examples 'project hook data with deprecateds'
+ include_examples 'deprecated repository hook data'
+ end
+
+ describe '.build' do
+ let(:data) do
+ described_class.build(project, user, Gitlab::Git::BLANK_SHA,
+ '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
+ 'refs/tags/v1.1.0')
+ end
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:before]).to eq(Gitlab::Git::BLANK_SHA) }
+ it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') }
+ it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
+ it { expect(data[:user_id]).to eq(user.id) }
+ it { expect(data[:user_name]).to eq(user.name) }
+ it { expect(data[:user_email]).to eq(user.email) }
+ it { expect(data[:user_avatar]).to eq(user.avatar_url) }
+ it { expect(data[:project_id]).to eq(project.id) }
+ it { expect(data[:project]).to be_a(Hash) }
+ it { expect(data[:commits]).to be_empty }
+ it { expect(data[:total_commits_count]).to be_zero }
+
+ include_examples 'project hook data with deprecateds'
+ include_examples 'deprecated repository hook data'
+
+ it 'does not raise an error when given nil commits' do
+ expect { described_class.build(spy, spy, spy, spy, spy, nil) }.
+ not_to raise_error
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 9096ad101b0..4ec3f19e03f 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -13,6 +13,10 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
context 'outside a transaction' do
before do
expect(model).to receive(:transaction_open?).and_return(false)
+
+ unless Gitlab::Database.postgresql?
+ allow_any_instance_of(Gitlab::Database::MigrationHelpers).to receive(:disable_statement_timeout)
+ end
end
context 'using PostgreSQL' do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 1cb513d5229..0650cb291e5 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -5,17 +5,17 @@ describe Gitlab::Diff::File, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
- describe :diff_lines do
+ describe '#diff_lines' do
let(:diff_lines) { diff_file.diff_lines }
it { expect(diff_lines.size).to eq(30) }
it { expect(diff_lines.first).to be_kind_of(Gitlab::Diff::Line) }
end
- describe :mode_changed? do
+ describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
@@ -32,4 +32,18 @@ describe Gitlab::Diff::File, lib: true do
expect(diff_file.too_large?).to eq(false)
end
end
+
+ describe '#collapsed?' do
+ it 'returns true for a file that is quite big' do
+ expect(diff).to receive(:collapsed?).and_return(true)
+
+ expect(diff_file.collapsed?).to eq(true)
+ end
+
+ it 'returns false for a file that is small enough' do
+ expect(diff).to receive(:collapsed?).and_return(false)
+
+ expect(diff_file.collapsed?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index fb5d50a5c68..1c2ddeed692 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Diff::Highlight, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
describe '#highlight' do
@@ -28,13 +28,13 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks removed lines' do
- code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+ code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">&quot;System commands must be given as an array of strings&quot;</span></span>\n}
+ code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 95a993d26cf..8ca3f73509e 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -3,14 +3,19 @@ require 'spec_helper'
describe Gitlab::Diff::InlineDiff, lib: true do
describe '.for_lines' do
let(:diff) do
- <<eos
- class Test
-- def initialize(test = true)
-+ def initialize(test = false)
- @test = test
- end
- end
-eos
+ <<-EOF.strip_heredoc
+ class Test
+ - def initialize(test = true)
+ + def initialize(test = false)
+ @test = test
+ - if true
+ - @foo = "bar"
+ + unless false
+ + @foo = "baz"
+ end
+ end
+ end
+ EOF
end
let(:subject) { described_class.for_lines(diff.lines) }
@@ -20,8 +25,11 @@ eos
expect(subject[1]).to eq([25..27])
expect(subject[2]).to eq([25..28])
expect(subject[3]).to be_nil
- expect(subject[4]).to be_nil
- expect(subject[5]).to be_nil
+ expect(subject[4]).to eq([5..10])
+ expect(subject[5]).to eq([17..17])
+ expect(subject[6]).to eq([5..15])
+ expect(subject[7]).to eq([17..17])
+ expect(subject[8]).to be_nil
end
end
diff --git a/spec/lib/gitlab/diff/line_mapper_spec.rb b/spec/lib/gitlab/diff/line_mapper_spec.rb
index 4e50e03bb7e..4b943fa382d 100644
--- a/spec/lib/gitlab/diff/line_mapper_spec.rb
+++ b/spec/lib/gitlab/diff/line_mapper_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Diff::LineMapper, lib: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
subject { described_class.new(diff_file) }
diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb
index 5f76b70c6f5..af18d3c25a6 100644
--- a/spec/lib/gitlab/diff/parallel_diff_spec.rb
+++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb
@@ -6,16 +6,56 @@ describe Gitlab::Diff::ParallelDiff, lib: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:commit) { project.commit(sample_commit.id) }
- let(:diffs) { commit.diffs }
+ let(:diffs) { commit.raw_diffs }
let(:diff) { diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) }
subject { described_class.new(diff_file) }
- let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") }
-
describe '#parallelize' do
it 'should return an array of arrays containing the parsed diff' do
- expect(subject.parallelize).to match_array(parallel_diff_result_array)
+ diff_lines = diff_file.highlighted_diff_lines
+ expected = [
+ # Unchanged lines
+ { left: diff_lines[0], right: diff_lines[0] },
+ { left: diff_lines[1], right: diff_lines[1] },
+ { left: diff_lines[2], right: diff_lines[2] },
+ { left: diff_lines[3], right: diff_lines[3] },
+ { left: diff_lines[4], right: diff_lines[5] },
+ { left: diff_lines[6], right: diff_lines[6] },
+ { left: diff_lines[7], right: diff_lines[7] },
+ { left: diff_lines[8], right: diff_lines[8] },
+
+ # Changed lines
+ { left: diff_lines[9], right: diff_lines[11] },
+ { left: diff_lines[10], right: diff_lines[12] },
+
+ # Added lines
+ { left: nil, right: diff_lines[13] },
+ { left: nil, right: diff_lines[14] },
+ { left: nil, right: diff_lines[15] },
+ { left: nil, right: diff_lines[16] },
+ { left: nil, right: diff_lines[17] },
+ { left: nil, right: diff_lines[18] },
+
+ # Unchanged lines
+ { left: diff_lines[19], right: diff_lines[19] },
+ { left: diff_lines[20], right: diff_lines[20] },
+ { left: diff_lines[21], right: diff_lines[21] },
+ { left: diff_lines[22], right: diff_lines[22] },
+ { left: diff_lines[23], right: diff_lines[23] },
+ { left: diff_lines[24], right: diff_lines[24] },
+ { left: diff_lines[25], right: diff_lines[25] },
+
+ # Added line
+ { left: nil, right: diff_lines[26] },
+
+ # Unchanged lines
+ { left: diff_lines[27], right: diff_lines[27] },
+ { left: diff_lines[28], right: diff_lines[28] },
+ { left: diff_lines[29], right: diff_lines[29] }
+ ]
+
+ expect(subject.parallelize).to eq(expected)
end
end
end
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index cdff063a9ed..b983d73f8be 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -5,10 +5,10 @@ describe Gitlab::Diff::Parser, lib: true do
let(:project) { create(:project) }
let(:commit) { project.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:diff) { commit.raw_diffs.first }
let(:parser) { Gitlab::Diff::Parser.new }
- describe :parse do
+ describe '#parse' do
let(:diff) do
<<eos
--- a/files/ruby/popen.rb
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index cf28628cb96..6e8fff6f516 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -338,4 +338,70 @@ describe Gitlab::Diff::Position, lib: true do
end
end
end
+
+ describe "position for a file in the initial commit" do
+ let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+ subject do
+ described_class.new(
+ old_path: "README.md",
+ new_path: "README.md",
+ old_line: nil,
+ new_line: 1,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+testme")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
+ describe "#to_json" do
+ let(:hash) do
+ {
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ base_sha: nil,
+ head_sha: nil,
+ start_sha: nil
+ }
+ end
+
+ let(:diff_position) { described_class.new(hash) }
+
+ it "returns the position as JSON" do
+ expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys)
+ end
+
+ it "works when nested under another hash" do
+ expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys)
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 08312e60f4a..c268f84c759 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1639,7 +1639,8 @@ describe Gitlab::Diff::PositionTracer, lib: true do
committer: committer
}
- repository.merge(current_user, second_create_file_commit.sha, branch_name, options)
+ merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
+ repository.merge(current_user, merge_request, options)
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb
new file mode 100644
index 00000000000..a5a398abf78
--- /dev/null
+++ b/spec/lib/gitlab/downtime_check/message_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::DowntimeCheck::Message do
+ describe '#to_s' do
+ it 'returns an ANSI formatted String for an offline migration' do
+ message = described_class.new('foo.rb', true, 'hello')
+
+ expect(message.to_s).to eq("[\e[31moffline\e[0m]: foo.rb:\n\nhello\n\n")
+ end
+
+ it 'returns an ANSI formatted String for an online migration' do
+ message = described_class.new('foo.rb')
+
+ expect(message.to_s).to eq("[\e[32monline\e[0m]: foo.rb")
+ end
+ end
+
+ describe '#reason?' do
+ it 'returns false when no reason is specified' do
+ message = described_class.new('foo.rb')
+
+ expect(message.reason?).to eq(false)
+ end
+
+ it 'returns true when a reason is specified' do
+ message = described_class.new('foo.rb', true, 'hello')
+
+ expect(message.reason?).to eq(true)
+ end
+ end
+
+ describe '#reason' do
+ it 'strips excessive whitespace from the returned String' do
+ message = described_class.new('foo.rb', true, " hello\n world\n\n foo")
+
+ expect(message.reason).to eq("hello\nworld\n\nfoo")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb
new file mode 100644
index 00000000000..42d895e548e
--- /dev/null
+++ b/spec/lib/gitlab/downtime_check_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Gitlab::DowntimeCheck do
+ subject { described_class.new }
+ let(:path) { 'foo.rb' }
+
+ describe '#check' do
+ before do
+ expect(subject).to receive(:require).with(path)
+ end
+
+ context 'when a migration does not specify if downtime is required' do
+ it 'raises RuntimeError' do
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(Class.new)
+
+ expect { subject.check([path]) }.
+ to raise_error(RuntimeError, /it requires downtime/)
+ end
+ end
+
+ context 'when a migration requires downtime' do
+ context 'when no reason is specified' do
+ it 'raises RuntimeError' do
+ stub_const('TestMigration::DOWNTIME', true)
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ expect { subject.check([path]) }.
+ to raise_error(RuntimeError, /no reason was given/)
+ end
+ end
+
+ context 'when a reason is specified' do
+ it 'returns an Array of messages' do
+ stub_const('TestMigration::DOWNTIME', true)
+ stub_const('TestMigration::DOWNTIME_REASON', 'foo')
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ messages = subject.check([path])
+
+ expect(messages).to be_an_instance_of(Array)
+ expect(messages[0]).to be_an_instance_of(Gitlab::DowntimeCheck::Message)
+
+ message = messages[0]
+
+ expect(message.path).to eq(path)
+ expect(message.offline).to eq(true)
+ expect(message.reason).to eq('foo')
+ end
+ end
+ end
+ end
+
+ describe '#check_and_print' do
+ it 'checks the migrations and prints the results to STDOUT' do
+ stub_const('TestMigration::DOWNTIME', true)
+ stub_const('TestMigration::DOWNTIME_REASON', 'foo')
+
+ expect(subject).to receive(:require).with(path)
+
+ expect(subject).to receive(:class_for_migration_file).
+ with(path).
+ and_return(TestMigration)
+
+ expect(subject).to receive(:puts).with(an_instance_of(String))
+
+ subject.check_and_print([path])
+ end
+ end
+
+ describe '#class_for_migration_file' do
+ it 'returns the class for a migration file path' do
+ expect(subject.class_for_migration_file('123_string.rb')).to eq(String)
+ end
+ end
+
+ describe '#online?' do
+ it 'returns true when a migration can be performed online' do
+ stub_const('TestMigration::DOWNTIME', false)
+
+ expect(subject.online?(TestMigration)).to eq(true)
+ end
+
+ it 'returns false when a migration can not be performed online' do
+ stub_const('TestMigration::DOWNTIME', true)
+
+ expect(subject.online?(TestMigration)).to eq(false)
+ end
+ end
+
+ describe '#downtime_reason' do
+ context 'when a reason is defined' do
+ it 'returns the downtime reason' do
+ stub_const('TestMigration::DOWNTIME_REASON', 'hello')
+
+ expect(subject.downtime_reason(TestMigration)).to eq('hello')
+ end
+ end
+
+ context 'when a reason is not defined' do
+ it 'returns nil' do
+ expect(subject.downtime_reason(Class.new)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb
index 476a21bf996..08b2577ecc4 100644
--- a/spec/lib/gitlab/email/attachment_uploader_spec.rb
+++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb
@@ -11,7 +11,6 @@ describe Gitlab::Email::AttachmentUploader, lib: true do
link = links.first
expect(link).not_to be_nil
- expect(link[:is_image]).to be_truthy
expect(link[:alt]).to eq("bricks")
expect(link[:url]).to include("bricks.png")
end
diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb
new file mode 100644
index 00000000000..19298e261e3
--- /dev/null
+++ b/spec/lib/gitlab/email/email_shared_blocks.rb
@@ -0,0 +1,41 @@
+require 'gitlab/email/receiver'
+
+shared_context :email_shared_context do
+ let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
+ let(:receiver) { Gitlab::Email::Receiver.new(email_raw) }
+ let(:markdown) { "![image](uploads/image.png)" }
+
+ def setup_attachment
+ allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
+ [
+ {
+ url: "uploads/image.png",
+ alt: "image",
+ markdown: markdown
+ }
+ ]
+ )
+ end
+end
+
+shared_examples :email_shared_examples do
+ context "when the user could not be found" do
+ before do
+ user.destroy
+ end
+
+ it "raises a UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when the user is not authorized to the project" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ it "raises a ProjectNotFound" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
new file mode 100644
index 00000000000..a5cc7b02936
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+ include_context :email_shared_context
+ it_behaves_like :email_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_new_issue.eml') }
+ let(:namespace) { create(:namespace, path: 'gitlabhq') }
+
+ let!(:project) { create(:project, :public, namespace: namespace) }
+ let!(:user) do
+ create(
+ :user,
+ email: 'jake@adventuretime.ooo',
+ authentication_token: 'auth_token'
+ )
+ end
+
+ context "when everything is fine" do
+ it "creates a new issue" do
+ setup_attachment
+
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to include('reply by email')
+ expect(issue.description).to include(markdown)
+ end
+
+ context "when the reply is blank" do
+ let(:email_raw) { fixture_file("emails/valid_new_issue_empty.eml") }
+
+ it "creates a new issue" do
+ expect { receiver.execute }.to change { project.issues.count }.by(1)
+ issue = project.issues.last
+
+ expect(issue.author).to eq(user)
+ expect(issue.title).to eq('New Issue by email')
+ expect(issue.description).to eq('')
+ end
+ end
+ end
+
+ context "something is wrong" do
+ context "when the issue could not be saved" do
+ before do
+ allow_any_instance_of(Issue).to receive(:persisted?).and_return(false)
+ end
+
+ it "raises an InvalidIssueError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidIssueError)
+ end
+ end
+
+ context "when we can't find the authentication_token" do
+ let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") }
+
+ it "raises an UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError)
+ end
+ end
+
+ context "when project is private" do
+ let(:project) { create(:project, :private, namespace: namespace) }
+
+ it "raises a ProjectNotFound if the user is not a member" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
new file mode 100644
index 00000000000..4909fed6b77
--- /dev/null
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -0,0 +1,177 @@
+require 'spec_helper'
+require_relative '../email_shared_blocks'
+
+describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
+ include_context :email_shared_context
+ it_behaves_like :email_shared_examples
+
+ before do
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
+ stub_config_setting(host: 'localhost')
+ end
+
+ let(:email_raw) { fixture_file('emails/valid_reply.eml') }
+ let(:project) { create(:project, :public) }
+ let(:noteable) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+
+ let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
+
+ context "when the recipient address doesn't include a mail key" do
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") }
+
+ it "raises a UnknownIncomingEmail" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
+
+ context "when no sent notification for the mail key could be found" do
+ let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
+
+ it "raises a SentNotificationNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError)
+ end
+ end
+
+ context "when the email was auto generated" do
+ let!(:mail_key) { '636ca428858779856c226bb145ef4fad' }
+ let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
+
+ it "raises an AutoGeneratedEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError)
+ end
+ end
+
+ context "when the noteable could not be found" do
+ before do
+ noteable.destroy
+ end
+
+ it "raises a NoteableNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError)
+ end
+ end
+
+ context "when the note could not be saved" do
+ before do
+ allow_any_instance_of(Note).to receive(:persisted?).and_return(false)
+ end
+
+ it "raises an InvalidNoteError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
+ end
+
+ context 'because the note was commands only' do
+ let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") }
+
+ context 'and current user cannot update noteable' do
+ it 'raises a CommandsOnlyNoteError' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
+ end
+ end
+
+ context 'and current user can update noteable' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not raise an error' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the 'close' event
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+ expect(noteable.reload).to be_closed
+ expect(noteable.due_date).to eq(Date.tomorrow)
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when the note contains slash commands' do
+ let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
+
+ context 'and current user cannot update noteable' do
+ it 'post a note and does not update the noteable' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the new note
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+
+ expect(noteable.reload).to be_open
+ expect(noteable.due_date).to be_nil
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+ end
+ end
+
+ context 'and current user can update noteable' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'post a note and updates the noteable' do
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
+
+ # One system note is created for the new note, one for the 'close' event
+ expect { receiver.execute }.to change { noteable.notes.count }.by(2)
+
+ expect(noteable.reload).to be_closed
+ expect(noteable.due_date).to eq(Date.tomorrow)
+ expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
+ end
+ end
+ end
+
+ context "when the reply is blank" do
+ let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
+
+ it "raises an EmptyEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
+ end
+ end
+
+ context "when everything is fine" do
+ before do
+ setup_attachment
+ end
+
+ it "creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include("I could not disagree more.")
+ end
+
+ it "adds all attachments" do
+ receiver.execute
+
+ note = noteable.notes.last
+
+ expect(note.note).to include(markdown)
+ end
+
+ context 'when sub-addressing is not supported' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: nil)
+ end
+
+ shared_examples 'an email that contains a mail key' do |header|
+ it "fetches the mail key from the #{header} header and creates a comment" do
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ note = noteable.notes.last
+
+ expect(note.author).to eq(sent_notification.recipient)
+ expect(note.note).to include('I could not disagree more.')
+ end
+ end
+
+ context 'mail key is in the References header' do
+ let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
+
+ it_behaves_like 'an email that contains a mail key', 'References'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
index c19f33e2224..5b966bddb6a 100644
--- a/spec/lib/gitlab/email/message/repository_push_spec.rb
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -16,9 +16,12 @@ describe Gitlab::Email::Message::RepositoryPush do
{ author_id: author.id, ref: 'master', action: :push, compare: compare,
send_from_committer_email: true }
end
- let(:compare) do
+ let(:raw_compare) do
Gitlab::Git::Compare.new(project.repository.raw_repository,
- sample_image_commit.id, sample_commit.id)
+ sample_image_commit.id, sample_commit.id)
+ end
+ let(:compare) do
+ Compare.decorate(raw_compare, project)
end
describe '#project' do
@@ -62,17 +65,17 @@ describe Gitlab::Email::Message::RepositoryPush do
describe '#diffs_count' do
subject { message.diffs_count }
- it { is_expected.to eq compare.diffs.count }
+ it { is_expected.to eq raw_compare.diffs.size }
end
describe '#compare' do
subject { message.compare }
- it { is_expected.to be_an_instance_of Gitlab::Git::Compare }
+ it { is_expected.to be_an_instance_of Compare }
end
describe '#compare_timeout' do
subject { message.compare_timeout }
- it { is_expected.to eq compare.diffs.overflow? }
+ it { is_expected.to eq raw_compare.diffs.overflow? }
end
describe '#reverse_compare?' do
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 36267faeb93..2a86b427806 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -1,34 +1,14 @@
-require "spec_helper"
+require 'spec_helper'
+require_relative 'email_shared_blocks'
describe Gitlab::Email::Receiver, lib: true do
- before do
- stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
- stub_config_setting(host: 'localhost')
- end
-
- let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
- let(:email_raw) { fixture_file('emails/valid_reply.eml') }
-
- let(:project) { create(:project, :public) }
- let(:noteable) { create(:issue, project: project) }
- let(:user) { create(:user) }
- let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) }
-
- let(:receiver) { described_class.new(email_raw) }
-
- context "when the recipient address doesn't include a reply key" do
- let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") }
-
- it "raises a SentNotificationNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
- end
- end
+ include_context :email_shared_context
- context "when no sent notificiation for the reply key could be found" do
- let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') }
+ context "when we cannot find a capable handler" do
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a SentNotificationNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
+ it "raises a UnknownIncomingEmail" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
end
@@ -36,129 +16,7 @@ describe Gitlab::Email::Receiver, lib: true do
let(:email_raw) { "" }
it "raises an EmptyEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
- end
- end
-
- context "when the email was auto generated" do
- let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
- let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
-
- it "raises an AutoGeneratedEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError)
- end
- end
-
- context "when the user could not be found" do
- before do
- user.destroy
- end
-
- it "raises a UserNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError)
- end
- end
-
- context "when the user has been blocked" do
- before do
- user.block
- end
-
- it "raises a UserBlockedError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError)
- end
- end
-
- context "when the user is not authorized to create a note" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- it "raises a UserNotAuthorizedError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError)
- end
- end
-
- context "when the noteable could not be found" do
- before do
- noteable.destroy
- end
-
- it "raises a NoteableNotFoundError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError)
- end
- end
-
- context "when the reply is blank" do
- let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
-
- it "raises an EmptyEmailError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
- end
- end
-
- context "when the note could not be saved" do
- before do
- allow_any_instance_of(Note).to receive(:persisted?).and_return(false)
- end
-
- it "raises an InvalidNoteError" do
- expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError)
- end
- end
-
- context "when everything is fine" do
- let(:markdown) { "![image](uploads/image.png)" }
-
- before do
- allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
- [
- {
- url: "uploads/image.png",
- is_image: true,
- alt: "image",
- markdown: markdown
- }
- ]
- )
- end
-
- it "creates a comment" do
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
-
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include("I could not disagree more.")
- end
-
- it "adds all attachments" do
- receiver.execute
-
- note = noteable.notes.last
-
- expect(note.note).to include(markdown)
- end
-
- context 'when sub-addressing is not supported' do
- before do
- stub_incoming_email_setting(enabled: true, address: nil)
- end
-
- shared_examples 'an email that contains a reply key' do |header|
- it "fetches the reply key from the #{header} header and creates a comment" do
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
- note = noteable.notes.last
-
- expect(note.author).to eq(sent_notification.recipient)
- expect(note.note).to include('I could not disagree more.')
- end
- end
-
- context 'reply key is in the References header' do
- let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
-
- it_behaves_like 'an email that contains a reply key', 'References'
- end
+ expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError)
end
end
end
diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb
index a15aa173fbd..d1f947b6850 100644
--- a/spec/lib/gitlab/git/hook_spec.rb
+++ b/spec/lib/gitlab/git/hook_spec.rb
@@ -25,7 +25,6 @@ describe Gitlab::Git::Hook, lib: true do
end
['pre-receive', 'post-receive', 'update'].each do |hook_name|
-
context "when triggering a #{hook_name} hook" do
context "when the hook is successful" do
it "returns success with no errors" do
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c79ba11f782..f12c9a370f7 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -6,67 +6,6 @@ describe Gitlab::GitAccess, lib: true do
let(:user) { create(:user) }
let(:actor) { user }
- describe 'can_push_to_branch?' do
- describe 'push to none protected branch' do
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?("random_branch")).to be_truthy
- end
-
- it "returns true if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?("random_branch")).to be_truthy
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?("random_branch")).to be_falsey
- end
- end
-
- describe 'push to protected branch' do
- before do
- @branch = create :protected_branch, project: project
- end
-
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns false if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
- end
-
- describe 'push to protected branch if allowed for developers' do
- before do
- @branch = create :protected_branch, project: project, developers_can_push: true
- end
-
- it "returns true if user is a master" do
- project.team << [user, :master]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns true if user is a developer" do
- project.team << [user, :developer]
- expect(access.can_push_to_branch?(@branch.name)).to be_truthy
- end
-
- it "returns false if user is a reporter" do
- project.team << [user, :reporter]
- expect(access.can_push_to_branch?(@branch.name)).to be_falsey
- end
- end
- end
-
describe '#check with single protocols allowed' do
def disable_protocol(protocol)
settings = ::ApplicationSetting.create_from_defaults
@@ -80,11 +19,11 @@ describe Gitlab::GitAccess, lib: true do
end
it 'blocks ssh git push' do
- expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end
it 'blocks ssh git pull' do
- expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end
end
@@ -95,22 +34,22 @@ describe Gitlab::GitAccess, lib: true do
end
it 'blocks http push' do
- expect(@acc.check('git-receive-pack').allowed?).to be_falsey
+ expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end
it 'blocks http git pull' do
- expect(@acc.check('git-upload-pack').allowed?).to be_falsey
+ expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end
end
end
describe 'download_access_check' do
+ subject { access.check('git-upload-pack', '_any') }
+
describe 'master permissions' do
before { project.team << [user, :master] }
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_truthy }
end
end
@@ -119,8 +58,6 @@ describe Gitlab::GitAccess, lib: true do
before { project.team << [user, :guest] }
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
@@ -132,16 +69,12 @@ describe Gitlab::GitAccess, lib: true do
end
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
describe 'without acccess to project' do
context 'pull code' do
- subject { access.download_access_check }
-
it { expect(subject.allowed?).to be_falsey }
end
end
@@ -151,113 +84,237 @@ describe Gitlab::GitAccess, lib: true do
let(:actor) { key }
context 'pull code' do
- before { key.projects << project }
- subject { access.download_access_check }
+ context 'when project is authorized' do
+ before { key.projects << project }
- it { expect(subject.allowed?).to be_truthy }
+ it { expect(subject).to be_allowed }
+ end
+
+ context 'when unauthorized' do
+ context 'from public project' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(subject).to be_allowed }
+ end
+
+ context 'from internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'from private project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+ end
end
end
end
describe 'push_access_check' do
- def protect_feature_branch
- create(:protected_branch, name: 'feature', project: project)
- end
+ before { merge_into_protected_branch }
+ let(:unprotected_branch) { FFaker::Internet.user_name }
- def changes
- {
- push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
+ let(:changes) do
+ { push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
push_master: '6f6d7e7ed 570e7b2ab refs/heads/master',
push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature',
push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\
'refs/heads/feature',
push_tag: '6f6d7e7ed 570e7b2ab refs/tags/v1.0.0',
push_new_tag: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/tags/v7.8.9",
- push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature']
- }
+ push_all: ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'],
+ merge_into_protected_branch: "0b4bc9a #{merge_into_protected_branch} refs/heads/feature" }
end
- def self.permissions_matrix
- {
- master: {
- push_new_branch: true,
- push_master: true,
- push_protected_branch: true,
- push_remove_protected_branch: false,
- push_tag: true,
- push_new_tag: true,
- push_all: true,
- },
-
- developer: {
- push_new_branch: true,
- push_master: true,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: true,
- push_all: false,
- },
-
- reporter: {
- push_new_branch: false,
- push_master: false,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: false,
- push_all: false,
- },
-
- guest: {
- push_new_branch: false,
- push_master: false,
- push_protected_branch: false,
- push_remove_protected_branch: false,
- push_tag: false,
- push_new_tag: false,
- push_all: false,
- }
- }
+ def stub_git_hooks
+ # Running the `pre-receive` hook is expensive, and not necessary for this test.
+ allow_any_instance_of(GitHooksService).to receive(:execute).and_yield
end
- def self.updated_permissions_matrix
- updated_permissions_matrix = permissions_matrix.dup
- updated_permissions_matrix[:developer][:push_protected_branch] = true
- updated_permissions_matrix[:developer][:push_all] = true
- updated_permissions_matrix
+ def merge_into_protected_branch
+ @protected_branch_merge_commit ||= begin
+ stub_git_hooks
+ project.repository.add_branch(user, unprotected_branch, 'feature')
+ target_branch = project.repository.lookup('feature')
+ source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false)
+ rugged = project.repository.rugged
+ author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
+
+ merge_index = rugged.merge_commits(target_branch, source_branch)
+ Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged))
+ end
end
- permissions_matrix.keys.each do |role|
- describe "#{role} access" do
- before { protect_feature_branch }
- before { project.team << [user, role] }
+ def self.run_permission_checks(permissions_matrix)
+ permissions_matrix.keys.each do |role|
+ describe "#{role} access" do
+ before do
+ if role == :admin
+ user.update_attribute(:admin, true)
+ else
+ project.team << [user, role]
+ end
+ end
- permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
+ permissions_matrix[role].each do |action, allowed|
+ context action do
+ subject { access.push_access_check(changes[action]) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ end
end
end
end
end
- context "with enabled developers push to protected branches " do
- updated_permissions_matrix.keys.each do |role|
- describe "#{role} access" do
- before { create(:protected_branch, name: 'feature', developers_can_push: true, project: project) }
- before { project.team << [user, role] }
+ permissions_matrix = {
+ admin: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: true,
+ push_remove_protected_branch: false,
+ push_tag: true,
+ push_new_tag: true,
+ push_all: true,
+ merge_into_protected_branch: true
+ },
+
+ master: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: true,
+ push_remove_protected_branch: false,
+ push_tag: true,
+ push_new_tag: true,
+ push_all: true,
+ merge_into_protected_branch: true
+ },
+
+ developer: {
+ push_new_branch: true,
+ push_master: true,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: true,
+ push_all: false,
+ merge_into_protected_branch: false
+ },
+
+ reporter: {
+ push_new_branch: false,
+ push_master: false,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: false,
+ push_all: false,
+ merge_into_protected_branch: false
+ },
+
+ guest: {
+ push_new_branch: false,
+ push_master: false,
+ push_protected_branch: false,
+ push_remove_protected_branch: false,
+ push_tag: false,
+ push_new_tag: false,
+ push_all: false,
+ merge_into_protected_branch: false
+ }
+ }
- updated_permissions_matrix[role].each do |action, allowed|
- context action do
- subject { access.push_access_check(changes[action]) }
+ [['feature', 'exact'], ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type|
+ context do
+ before { create(:protected_branch, name: protected_branch_name, project: project) }
- it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
+ run_permission_checks(permissions_matrix)
+ end
+
+ context "when developers are allowed to push into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
+ end
+
+ context "developers are allowed to merge into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) }
+
+ context "when a merge request exists for the given source/target branch" do
+ context "when the merge request is in progress" do
+ before do
+ create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature',
+ state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch)
end
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true }))
+ end
+
+ context "when the merge request is not in progress" do
+ before do
+ create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', in_progress_merge_commit_sha: nil)
+ end
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
+ end
+
+ context "when a merge request does not exist for the given source/target branch" do
+ run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false }))
end
end
end
+
+ context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do
+ before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true }))
+ end
+
+ context "when no one is allowed to push to the #{protected_branch_name} protected branch" do
+ before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) }
+
+ run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
+ master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false },
+ admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
+ end
+ end
+ end
+
+ describe 'deploy key permissions' do
+ let(:key) { create(:deploy_key) }
+ let(:actor) { key }
+
+ context 'push code' do
+ subject { access.check('git-receive-pack', '_any') }
+
+ context 'when project is authorized' do
+ before { key.projects << project }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'when unauthorized' do
+ context 'to public project' do
+ let(:project) { create(:project, :public) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'to internal project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+
+ context 'to private project' do
+ let(:project) { create(:project, :internal) }
+
+ it { expect(subject).not_to be_allowed }
+ end
+ 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 fc9d5204148..e5300dbba1e 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -32,20 +32,6 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
end
end
- describe '#name' do
- it 'returns raw ref when branch exists' do
- branch = described_class.new(project, double(raw))
-
- expect(branch.name).to eq 'feature'
- end
-
- it 'returns formatted ref when branch does not exist' do
- branch = described_class.new(project, double(raw.merge(ref: 'removed-branch', sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
-
- expect(branch.name).to eq 'removed-branch-2e5d3239'
- end
- end
-
describe '#repo' do
it 'returns raw repo' do
branch = described_class.new(project, double(raw))
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 3b023a35446..613c47d55f1 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -61,4 +61,11 @@ describe Gitlab::GithubImport::Client, lib: true do
expect(client.api.api_endpoint).to eq 'https://github.company.com/'
end
end
+
+ it 'does not raise error when rate limit is disabled' do
+ stub_request(:get, /api.github.com/)
+ allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+
+ expect { client.issues }.not_to raise_error
+ end
end
diff --git a/spec/lib/gitlab/github_import/hook_formatter_spec.rb b/spec/lib/gitlab/github_import/hook_formatter_spec.rb
deleted file mode 100644
index 110ba428258..00000000000
--- a/spec/lib/gitlab/github_import/hook_formatter_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GithubImport::HookFormatter, lib: true do
- describe '#id' do
- it 'returns raw id' do
- raw = double(id: 100000)
- formatter = described_class.new(raw)
- expect(formatter.id).to eq 100000
- end
- end
-
- describe '#name' do
- it 'returns raw id' do
- raw = double(name: 'web')
- formatter = described_class.new(raw)
- expect(formatter.name).to eq 'web'
- end
- end
-
- describe '#config' do
- it 'returns raw config.attrs' do
- raw = double(config: double(attrs: { url: 'http://something.com/webhook' }))
- formatter = described_class.new(raw)
- expect(formatter.config).to eq({ url: 'http://something.com/webhook' })
- end
- end
-
- describe '#valid?' do
- it 'returns true when events contains the wildcard event' do
- raw = double(events: ['*', 'commit_comment'], active: true)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq true
- end
-
- it 'returns true when events contains the create event' do
- raw = double(events: ['create', 'commit_comment'], active: true)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq true
- end
-
- it 'returns true when events contains delete event' do
- raw = double(events: ['delete', 'commit_comment'], active: true)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq true
- end
-
- it 'returns true when events contains pull_request event' do
- raw = double(events: ['pull_request', 'commit_comment'], active: true)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq true
- end
-
- it 'returns false when events does not contains branch related events' do
- raw = double(events: ['member', 'commit_comment'], active: true)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq false
- end
-
- it 'returns false when hook is not active' do
- raw = double(events: ['pull_request', 'commit_comment'], active: false)
- formatter = described_class.new(raw)
- expect(formatter.valid?).to eq false
- end
- end
-end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
new file mode 100644
index 00000000000..b7c3bc4e1a7
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer, lib: true do
+ describe '#execute' do
+ context 'when an error occurs' do
+ let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+
+ let(:label) do
+ double(
+ name: 'Bug',
+ color: 'ff0000',
+ url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+ )
+ end
+
+ let(:milestone) do
+ double(
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
+ )
+ end
+
+ let(:issue1) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
+ )
+ end
+
+ let(:issue2) do
+ double(
+ number: 1348,
+ milestone: nil,
+ state: 'open',
+ title: nil,
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
+ )
+ end
+
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ )
+ end
+
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+ allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label])
+ allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+ allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+ end
+
+ it 'returns true' do
+ expect(described_class.new(project).execute).to eq true
+ end
+
+ it 'does not raise an error' do
+ expect { described_class.new(project).execute }.not_to raise_error
+ end
+
+ it 'stores error messages' do
+ error = {
+ 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 has already been taken" },
+ { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/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, 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: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
+ { type: :wiki, errors: "Gitlab::Shell::Error" }
+ ]
+ }
+
+ described_class.new(project).execute
+
+ expect(project.import_error).to eq error.to_json
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 79931ecd134..b667abf063d 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -9,6 +9,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
+ let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
let(:octocat) { double(id: 123456, login: 'octocat') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -26,7 +27,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- merged_at: nil
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
}
end
@@ -165,6 +167,42 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ describe '#source_branch_name' do
+ context 'when source branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.source_branch_name).to eq 'feature'
+ end
+ end
+
+ context 'when source branch does not exist' do
+ let(:raw_data) { double(base_data.merge(head: removed_branch)) }
+
+ it 'prefixes branch name with pull request number' do
+ expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
+ end
+ end
+ end
+
+ describe '#target_branch_name' do
+ context 'when source branch exists' do
+ let(:raw_data) { double(base_data) }
+
+ it 'returns branch ref' do
+ expect(pull_request.target_branch_name).to eq 'master'
+ end
+ end
+
+ context 'when target branch does not exist' do
+ let(:raw_data) { double(base_data.merge(base: removed_branch)) }
+
+ it 'prefixes branch name with pull request number' do
+ expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch'
+ end
+ end
+ end
+
describe '#valid?' do
context 'when source, and target repos are not a fork' do
let(:raw_data) { double(base_data) }
@@ -178,8 +216,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:source_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
- it 'returns false' do
- expect(pull_request.valid?).to eq false
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
end
end
@@ -187,9 +225,17 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_repo) { double(id: 2) }
let(:raw_data) { double(base_data) }
- it 'returns false' do
- expect(pull_request.valid?).to eq false
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
end
end
end
+
+ describe '#url' do
+ let(:raw_data) { double(base_data) }
+
+ it 'return raw url' do
+ expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
new file mode 100644
index 00000000000..d3f1deb3837
--- /dev/null
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Gitlab::GitlabImport::Importer, lib: true do
+ include ImportSpecHelper
+
+ describe '#execute' do
+ before do
+ stub_omniauth_provider('gitlab')
+ stub_request('issues', [
+ {
+ 'id' => 2579857,
+ 'iid' => 3,
+ 'title' => 'Issue',
+ 'description' => 'Lorem ipsum',
+ 'state' => 'opened',
+ 'author' => {
+ 'id' => 283999,
+ 'name' => 'John Doe'
+ }
+ }
+ ])
+ stub_request('issues/2579857/notes', [])
+ end
+
+ it 'persists issues' do
+ project = create(:empty_project, import_source: 'asd/vim')
+ project.build_import_data(credentials: { password: 'password' })
+
+ subject = described_class.new(project)
+ subject.execute
+
+ expected_attributes = {
+ iid: 3,
+ title: 'Issue',
+ description: "*Created by: John Doe*\n\nLorem ipsum",
+ state: 'opened',
+ author_id: project.creator_id
+ }
+
+ expect(project.issues.first).to have_attributes(expected_attributes)
+ end
+
+ def stub_request(path, body)
+ url = "https://gitlab.com/api/v3/projects/asd%2Fvim/#{path}?page=1&per_page=100"
+
+ WebMock.stub_request(:get, url).
+ to_return(
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
deleted file mode 100644
index 946712ca38e..00000000000
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
- let(:user) { create(:user) }
- let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
- let(:namespace){ create(:group, owner: user) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
-
- project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.name).to eq("Bar Baz Qux")
- expect(project.path).to eq("bar-baz-qux")
- expect(project.namespace).to eq(namespace)
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- expect(project.import_type).to eq("gitorious")
- expect(project.import_source).to eq("foo/bar-baz-qux")
- expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git")
- end
-end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index 364532e94e3..fc021416d92 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -17,6 +17,18 @@ describe Gitlab::Highlight, lib: true do
expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end
+
+ describe 'with CRLF' do
+ let(:branch) { 'crlf-diff' }
+ let(:blob) { repository.blob_at_branch(branch, path) }
+ let(:lines) do
+ Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace')
+ end
+
+ it 'strips extra LFs' do
+ expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>")
+ end
+ end
end
describe 'custom highlighting from .gitattributes' do
diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
new file mode 100644
index 00000000000..5ae178414cc
--- /dev/null
+++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AvatarRestorer, lib: true do
+ 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")
+ end
+
+ after do
+ project.remove_avatar!
+ end
+
+ it 'restores a project avatar' do
+ expect(described_class.new(project: project, shared: shared).restore).to be true
+ end
+
+ it 'saves the avatar into the project' do
+ described_class.new(project: project, shared: shared).restore
+
+ expect(project.reload.avatar.file.exists?).to be true
+ end
+end
diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
new file mode 100644
index 00000000000..d6ee94442cb
--- /dev/null
+++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AvatarSaver, lib: true do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
+ let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" }
+ let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ FileUtils.mkdir_p("#{shared.export_path}/avatar/")
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ end
+
+ after do
+ FileUtils.rm_rf("#{shared.export_path}/avatar")
+ end
+
+ it 'saves a project avatar' do
+ described_class.new(project: project_with_avatar, shared: shared).save
+
+ expect(File).to exist("#{shared.export_path}/avatar/dk.png")
+ end
+
+ it 'is fine not to have an avatar' do
+ expect(described_class.new(project: project, shared: shared).save).to be true
+ end
+end
diff --git a/spec/lib/gitlab/import_export/import_export_spec.rb b/spec/lib/gitlab/import_export/import_export_spec.rb
new file mode 100644
index 00000000000..d6409a29550
--- /dev/null
+++ b/spec/lib/gitlab/import_export/import_export_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport, services: true do
+ describe 'export filename' do
+ let(:project) { create(:project, :public, path: 'project-path') }
+
+ it 'contains the project path' do
+ expect(described_class.export_filename(project: project)).to include(project.path)
+ end
+
+ it 'contains the namespace path' do
+ expect(described_class.export_filename(project: project)).to include(project.namespace.path)
+ end
+
+ it 'does not go over a certain length' do
+ project.path = 'a' * 100
+
+ expect(described_class.export_filename(project: project).length).to be < 70
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 6d5aa0d04a2..770e8b0c2f4 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -26,6 +26,20 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
"email" => user2.email,
"username" => user2.username
}
+ },
+ {
+ "id" => 3,
+ "access_level" => 40,
+ "source_id" => 14,
+ "source_type" => "Project",
+ "user_id" => nil,
+ "notification_level" => 3,
+ "created_at" => "2016-03-11T10:21:44.822Z",
+ "updated_at" => "2016-03-11T10:21:44.822Z",
+ "created_by_id" => 1,
+ "invite_email" => 'invite@test.com',
+ "invite_token" => 'token',
+ "invite_accepted_at" => nil
}]
end
@@ -47,5 +61,11 @@ describe Gitlab::ImportExport::MembersMapper, services: true do
expect(members_mapper.missing_author_ids.first).to eq(-1)
end
+
+ it 'has invited members with no user' do
+ members_mapper.map
+
+ expect(ProjectMember.find_by_invite_email('invite@test.com')).not_to be_nil
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 0b30e8c9b04..cbbf98dca94 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -18,7 +18,6 @@
"position": 0,
"branch_name": null,
"description": "Aliquam enim illo et possimus.",
- "milestone_id": 18,
"state": "opened",
"iid": 10,
"updated_by_id": null,
@@ -26,6 +25,53 @@
"deleted_at": null,
"due_date": null,
"moved_to_id": null,
+ "test_ee_field": "test",
+ "milestone": {
+ "id": 1,
+ "title": "v0.0",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
+ "label_links": [
+ {
+ "id": 2,
+ "label_id": 2,
+ "target_id": 3,
+ "target_type": "Issue",
+ "created_at": "2016-07-22T08:57:02.840Z",
+ "updated_at": "2016-07-22T08:57:02.840Z",
+ "label": {
+ "id": 2,
+ "title": "test2",
+ "color": "#428bca",
+ "project_id": 8,
+ "created_at": "2016-07-22T08:55:44.161Z",
+ "updated_at": "2016-07-22T08:55:44.161Z",
+ "template": false,
+ "description": "",
+ "priority": null
+ }
+ }
+ ],
"notes": [
{
"id": 351,
@@ -232,7 +278,6 @@
"position": 0,
"branch_name": null,
"description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.",
- "milestone_id": 16,
"state": "opened",
"iid": 9,
"updated_by_id": null,
@@ -446,7 +491,6 @@
"position": 0,
"branch_name": null,
"description": "Ea recusandae neque autem tempora.",
- "milestone_id": 16,
"state": "closed",
"iid": 8,
"updated_by_id": null,
@@ -660,7 +704,6 @@
"position": 0,
"branch_name": null,
"description": "Maiores architecto quos in dolorem.",
- "milestone_id": 17,
"state": "opened",
"iid": 7,
"updated_by_id": null,
@@ -874,7 +917,6 @@
"position": 0,
"branch_name": null,
"description": "Ut aut ut et tenetur velit aut id modi.",
- "milestone_id": 16,
"state": "opened",
"iid": 6,
"updated_by_id": null,
@@ -1088,7 +1130,6 @@
"position": 0,
"branch_name": null,
"description": "Dicta nisi nihil non ipsa velit.",
- "milestone_id": 20,
"state": "closed",
"iid": 5,
"updated_by_id": null,
@@ -1302,7 +1343,6 @@
"position": 0,
"branch_name": null,
"description": "Ut et explicabo vel voluptatem consequuntur ut sed.",
- "milestone_id": 19,
"state": "closed",
"iid": 4,
"updated_by_id": null,
@@ -1516,7 +1556,6 @@
"position": 0,
"branch_name": null,
"description": "Non asperiores velit accusantium voluptate.",
- "milestone_id": 18,
"state": "closed",
"iid": 3,
"updated_by_id": null,
@@ -1730,7 +1769,6 @@
"position": 0,
"branch_name": null,
"description": "Molestiae corporis magnam et fugit aliquid nulla quia.",
- "milestone_id": 17,
"state": "closed",
"iid": 2,
"updated_by_id": null,
@@ -1944,7 +1982,6 @@
"position": 0,
"branch_name": null,
"description": "Quod ad architecto qui est sed quia.",
- "milestone_id": 20,
"state": "closed",
"iid": 1,
"updated_by_id": null,
@@ -2258,117 +2295,6 @@
"author_id": 25
}
]
- },
- {
- "id": 18,
- "title": "v2.0",
- "project_id": 5,
- "description": "Error dolorem rerum aut nulla.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.576Z",
- "updated_at": "2016-06-14T15:02:04.576Z",
- "state": "active",
- "iid": 3,
- "events": [
- {
- "id": 242,
- "target_type": "Milestone",
- "target_id": 18,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.579Z",
- "updated_at": "2016-06-14T15:02:04.579Z",
- "action": 1,
- "author_id": 1
- },
- {
- "id": 58,
- "target_type": "Milestone",
- "target_id": 18,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.579Z",
- "updated_at": "2016-06-14T15:02:04.579Z",
- "action": 1,
- "author_id": 22
- }
- ]
- },
- {
- "id": 17,
- "title": "v1.0",
- "project_id": 5,
- "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.569Z",
- "updated_at": "2016-06-14T15:02:04.569Z",
- "state": "active",
- "iid": 2,
- "events": [
- {
- "id": 243,
- "target_type": "Milestone",
- "target_id": 17,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.570Z",
- "updated_at": "2016-06-14T15:02:04.570Z",
- "action": 1,
- "author_id": 1
- },
- {
- "id": 57,
- "target_type": "Milestone",
- "target_id": 17,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.570Z",
- "updated_at": "2016-06-14T15:02:04.570Z",
- "action": 1,
- "author_id": 20
- }
- ]
- },
- {
- "id": 16,
- "title": "v0.0",
- "project_id": 5,
- "description": "Velit numquam et sed sit.",
- "due_date": null,
- "created_at": "2016-06-14T15:02:04.561Z",
- "updated_at": "2016-06-14T15:02:04.561Z",
- "state": "closed",
- "iid": 1,
- "events": [
- {
- "id": 244,
- "target_type": "Milestone",
- "target_id": 16,
- "title": null,
- "data": null,
- "project_id": 36,
- "created_at": "2016-06-14T15:02:04.563Z",
- "updated_at": "2016-06-14T15:02:04.563Z",
- "action": 1,
- "author_id": 26
- },
- {
- "id": 56,
- "target_type": "Milestone",
- "target_id": 16,
- "title": null,
- "data": null,
- "project_id": 5,
- "created_at": "2016-06-14T15:02:04.563Z",
- "updated_at": "2016-06-14T15:02:04.563Z",
- "action": 1,
- "author_id": 26
- }
- ]
}
],
"snippets": [
@@ -2467,10 +2393,9 @@
"source_project_id": 5,
"author_id": 1,
"assignee_id": null,
- "title": "Cannot be automatically merged",
+ "title": "MR1",
"created_at": "2016-06-14T15:02:36.568Z",
"updated_at": "2016-06-14T15:02:56.815Z",
- "milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -2764,7 +2689,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -2902,13 +2827,12 @@
"id": 26,
"target_branch": "master",
"source_branch": "feature",
- "source_project_id": 5,
+ "source_project_id": 4,
"author_id": 1,
"assignee_id": null,
- "title": "Can be automatically merged",
+ "title": "MR2",
"created_at": "2016-06-14T15:02:36.418Z",
"updated_at": "2016-06-14T15:02:57.013Z",
- "milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3137,7 +3061,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n",
"new_path": "files/ruby/feature.rb",
@@ -3193,7 +3117,6 @@
"title": "Qui accusantium et inventore facilis doloribus occaecati officiis.",
"created_at": "2016-06-14T15:02:25.168Z",
"updated_at": "2016-06-14T15:02:59.521Z",
- "milestone_id": 17,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3422,7 +3345,7 @@
"committer_email": "james@jameslopez.es"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
@@ -3478,7 +3401,6 @@
"title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.",
"created_at": "2016-06-14T15:02:24.760Z",
"updated_at": "2016-06-14T15:02:59.749Z",
- "milestone_id": 20,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -3959,7 +3881,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -4169,7 +4091,6 @@
"title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.",
"created_at": "2016-06-14T15:02:24.415Z",
"updated_at": "2016-06-14T15:02:59.958Z",
- "milestone_id": 17,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -4208,7 +4129,18 @@
"name": "User 4"
},
"events": [
-
+ {
+ "id": 529,
+ "target_type": "Note",
+ "target_id": 2521,
+ "title": "test levels",
+ "data": null,
+ "project_id": 4,
+ "created_at": "2016-07-07T14:35:12.128Z",
+ "updated_at": "2016-07-07T14:35:12.128Z",
+ "action": 6,
+ "author_id": 1
+ }
]
},
{
@@ -4585,7 +4517,7 @@
"committer_email": "marmis85@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
@@ -4707,7 +4639,6 @@
"title": "In a rerum harum nihil accusamus aut quia nobis non.",
"created_at": "2016-06-14T15:02:24.000Z",
"updated_at": "2016-06-14T15:03:00.225Z",
- "milestone_id": 19,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5096,7 +5027,7 @@
"committer_email": "stanhu@packetzoom.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n",
"new_path": "CHANGELOG",
@@ -5207,7 +5138,6 @@
"title": "Corporis provident similique perspiciatis dolores eos animi.",
"created_at": "2016-06-14T15:02:23.767Z",
"updated_at": "2016-06-14T15:03:00.475Z",
- "milestone_id": 18,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5422,7 +5352,7 @@
"id": 11,
"state": "empty",
"st_commits": null,
- "st_diffs": [
+ "utf8_st_diffs": [
],
"merge_request_id": 11,
@@ -5468,7 +5398,6 @@
"title": "Eligendi reprehenderit doloribus quia et sit id.",
"created_at": "2016-06-14T15:02:23.014Z",
"updated_at": "2016-06-14T15:03:00.685Z",
- "milestone_id": 20,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -5949,7 +5878,7 @@
"committer_email": "dmitriy.zaporozhets@gmail.com"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "Binary files a/.DS_Store and /dev/null differ\n",
"new_path": ".DS_Store",
@@ -6159,7 +6088,6 @@
"title": "Et ipsam voluptas velit sequi illum ut.",
"created_at": "2016-06-14T15:02:22.825Z",
"updated_at": "2016-06-14T15:03:00.904Z",
- "milestone_id": 16,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 5,
@@ -6388,7 +6316,7 @@
"committer_email": "james@jameslopez.es"
}
],
- "st_diffs": [
+ "utf8_st_diffs": [
{
"diff": "--- /dev/null\n+++ b/test\n",
"new_path": "test",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index a72aaa44e82..4d857945fde 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -24,11 +24,75 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Ci::Pipeline.first.notes).not_to be_empty
end
- it 'restores the correct event' do
+ it 'restores the correct event with symbolised data' do
restored_project_json
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end
+
+ it 'preserves updated_at on issues' do
+ restored_project_json
+
+ issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
+
+ expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
+ end
+
+ context 'event at forth level of the tree' do
+ let(:event) { Event.where(title: 'test levels').first }
+
+ before do
+ restored_project_json
+ end
+
+ it 'restores the event' do
+ expect(event).not_to be_nil
+ end
+
+ it 'event belongs to note, belongs to merge request, belongs to a project' do
+ expect(event.note.noteable.project).not_to be_nil
+ end
+ end
+
+ it 'has the correct data for merge request st_diffs' do
+ # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
+
+ expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9)
+ end
+
+ it 'has labels associated to label links, associated to issues' do
+ restored_project_json
+
+ expect(Label.first.label_links.first.target).not_to be_nil
+ end
+
+ it 'has milestones associated to issues' do
+ restored_project_json
+
+ expect(Milestone.find_by_description('test milestone').issues).not_to be_empty
+ end
+
+ context 'Merge requests' do
+ before do
+ restored_project_json
+ end
+
+ it 'always has the new project as a target' do
+ expect(MergeRequest.find_by_title('MR1').target_project).to eq(project)
+ end
+
+ it 'has the same source project as originally if source/target are the same' do
+ expect(MergeRequest.find_by_title('MR1').source_project).to eq(project)
+ end
+
+ it 'has the new project as target if source/target differ' do
+ expect(MergeRequest.find_by_title('MR2').target_project).to eq(project)
+ end
+
+ it 'has no source if source/target differ' do
+ expect(MergeRequest.find_by_title('MR2').source_project_id).to eq(-1)
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 1424de9e60b..3a86a4ce07c 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -31,10 +31,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json).to include({ "visibility_level" => 20 })
end
- it 'has events' do
- expect(saved_project_json['milestones'].first['events']).not_to be_empty
- end
-
it 'has milestones' do
expect(saved_project_json['milestones']).not_to be_empty
end
@@ -43,8 +39,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['merge_requests']).not_to be_empty
end
- it 'has labels' do
- expect(saved_project_json['labels']).not_to be_empty
+ it 'has merge request\'s milestones' do
+ expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty
+ end
+
+ it 'has events' do
+ expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty
end
it 'has snippets' do
@@ -102,25 +102,38 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
it 'has ci pipeline notes' do
expect(saved_project_json['pipelines'].first['notes']).not_to be_empty
end
+
+ it 'has labels with no associations' do
+ expect(saved_project_json['labels']).not_to be_empty
+ end
+
+ it 'has labels associated to records' do
+ expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
+ end
+
+ it 'does not complain about non UTF-8 characters in MR diffs' do
+ ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
+
+ expect(project_tree_saver.save).to be true
+ end
end
end
def setup_project
issue = create(:issue, assignee: user)
- merge_request = create(:merge_request)
- label = create(:label)
snippet = create(:project_snippet)
release = create(:release)
project = create(:project,
:public,
issues: [issue],
- merge_requests: [merge_request],
- labels: [label],
snippets: [snippet],
releases: [release]
)
-
+ label = create(:label, project: project)
+ create(:label_link, label: label, target: issue)
+ milestone = create(:milestone, project: project)
+ merge_request = create(:merge_request, source_project: project, milestone: milestone)
commit_status = create(:commit_status, project: project)
ci_pipeline = create(:ci_pipeline,
@@ -130,7 +143,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
statuses: [commit_status])
create(:ci_build, pipeline: ci_pipeline, project: project)
- milestone = create(:milestone, project: project)
+ create(:milestone, project: project)
create(:note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
create(:note, noteable: snippet, project: project)
diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb
index b76e14deca1..b6dec41d218 100644
--- a/spec/lib/gitlab/import_export/reader_spec.rb
+++ b/spec/lib/gitlab/import_export/reader_spec.rb
@@ -12,7 +12,8 @@ describe Gitlab::ImportExport::Reader, lib: true do
except: [:iid],
include: [:merge_request_diff, :merge_request_test]
} },
- { commit_statuses: { include: :commit } }]
+ { commit_statuses: { include: :commit } },
+ { project_members: { include: { user: { only: [:email] } } } }]
}
end
diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb
new file mode 100644
index 00000000000..90c6d1c67f6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/version_checker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::VersionChecker, services: true do
+ describe 'bundle a project Git repo' do
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') }
+ let(:version) { Gitlab::ImportExport.version }
+
+ before do
+ allow(File).to receive(:open).and_return(version)
+ end
+
+ it 'returns true if Import/Export have the same version' do
+ expect(described_class.check!(shared: shared)).to be true
+ end
+
+ context 'newer version' do
+ let(:version) { '900.0'}
+
+ it 'returns false if export version is newer' do
+ expect(described_class.check!(shared: shared)).to be false
+ end
+
+ it 'shows the correct error message' do
+ described_class.check!(shared: shared)
+
+ expect(shared.errors.first).to eq("Import version mismatch: Required <= #{Gitlab::ImportExport.version} but was #{version}")
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index afb3e26f8fb..1dcf2c0668b 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -43,9 +43,9 @@ describe Gitlab::IncomingEmail, lib: true do
end
end
- context 'self.key_from_fallback_reply_message_id' do
+ context 'self.key_from_fallback_message_id' do
it 'returns reply key' do
- expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
+ expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
end
end
end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index f5b66b8156f..534bcbf39fe 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::LDAP::Access, lib: true do
let(:access) { Gitlab::LDAP::Access.new user }
let(:user) { create(:omniauth_user) }
- describe :allowed? do
+ describe '#allowed?' do
subject { access.allowed? }
context 'when the user cannot be found' do
@@ -64,7 +64,7 @@ describe Gitlab::LDAP::Access, lib: true do
user.ldap_block
end
- it 'should unblock user in GitLab' do
+ it 'unblocks user in GitLab' do
access.allowed?
expect(user).not_to be_blocked
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 03199a2523e..89790c9e1af 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::LDAP::User, lib: true do
OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case)
end
- describe :changed? do
+ describe '#changed?' do
it "marks existing ldap user as changed" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
expect(ldap_user.changed?).to be_truthy
@@ -36,7 +36,7 @@ describe Gitlab::LDAP::User, lib: true do
expect(ldap_user.changed?).to be_truthy
end
- it "dont marks existing ldap user as changed" do
+ it "does not mark existing ldap user as changed" do
create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true)
expect(ldap_user.changed?).to be_falsey
end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
deleted file mode 100644
index 659facd6c19..00000000000
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ /dev/null
@@ -1,730 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Lfs::Router, lib: true do
- let(:project) { create(:project) }
- let(:public_project) { create(:project, :public) }
- let(:forked_project) { fork_project(public_project, user) }
-
- let(:user) { create(:user) }
- let(:user_two) { create(:user) }
- let!(:lfs_object) { create(:lfs_object, :with_file) }
-
- let(:request) { Rack::Request.new(env) }
- let(:env) do
- {
- 'rack.input' => '',
- 'REQUEST_METHOD' => 'GET',
- }
- end
-
- let(:lfs_router_auth) { new_lfs_router(project, user: user) }
- let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
- let(:lfs_router_noauth) { new_lfs_router(project) }
- let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
- let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
- let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
- let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
- let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
- let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
-
- let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
- let(:sample_size) { 499013 }
- let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
- let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]}
-
- describe 'when lfs is disabled' do
- before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
- env['REQUEST_METHOD'] = 'POST'
- body = {
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ],
- 'operation' => 'upload'
- }.to_json
- env['rack.input'] = StringIO.new(body)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
- end
- end
-
- describe 'when fetching lfs object using deprecated API' do
- before do
- enable_lfs
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
- end
- end
-
- describe 'when fetching lfs object' do
- before do
- enable_lfs
- env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
- end
-
- describe 'and request comes from gitlab-workhorse' do
- context 'without user being authorized' do
- it "responds with status 401" do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'with required headers' do
- before do
- project.lfs_objects << lfs_object
- env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
- end
-
- context 'when user does not have project access' do
- it "responds with status 403" do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when user has project access' do
- before do
- project.team << [user, :master]
- end
-
- it "responds with status 200" do
- expect(lfs_router_auth.try_call.first).to eq(200)
- end
-
- it "responds with the file location" do
- expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
- expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
- end
- end
-
- context 'when CI is authorized' do
- it "responds with status 200" do
- expect(lfs_router_ci_auth.try_call.first).to eq(200)
- end
-
- it "responds with the file location" do
- expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
- expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
- end
- end
- end
-
- context 'without required headers' do
- it "responds with status 403" do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
- end
-
- describe 'when handling lfs request using deprecated API' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'POST'
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects"
- end
-
- it 'responds with 501' do
- expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
- end
- end
-
- describe 'when handling lfs batch request' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'POST'
- env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
- end
-
- describe 'download' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- shared_examples 'an authorized requests' do
- context 'when downloading an lfs object that is assigned to our project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => auth }
- }
- }
- }])
- end
- end
-
- context 'when downloading an lfs object that is assigned to other project' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and error message' do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
- end
-
- context 'when downloading a lfs object that does not exist' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it "responds with status 200 and error message" do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
- end
-
- context 'when downloading one new and one existing lfs object' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = router.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => auth }
- }
- }
- }])
- end
- end
- end
-
- context 'when user is authenticated' do
- let(:auth) { authorize(user) }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- project.team << [user, role]
- end
-
- it_behaves_like 'an authorized requests' do
- let(:role) { :reporter }
- let(:router) { lfs_router_auth }
- end
-
- context 'when user does is not member of the project' do
- let(:role) { :guest }
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when user does not have download access' do
- let(:role) { :guest }
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
-
- context 'when CI is authorized' do
- let(:auth) { 'gitlab-ci-token:password' }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- end
-
- it_behaves_like 'an authorized requests' do
- let(:router) { lfs_router_ci_auth }
- end
- end
-
- context 'when user is not authenticated' do
- describe 'is accessing public project' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = lfs_router_public_noauth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
-
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => {}
- }
- }
- }])
- end
- end
-
- describe 'is accessing non-public project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with authorization required' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
- end
- end
-
- describe 'upload' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- describe 'when request is authenticated' do
- describe 'when user has project push access' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :developer]
- end
-
- context 'when pushing an lfs object that already exists' do
- before do
- public_project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 and links the object to the project" do
- response_body = lfs_router_auth.try_call.last
- response = ActiveSupport::JSON.decode(response_body.first)
-
- expect(response['objects']).to be_kind_of(Array)
- expect(response['objects'].first['oid']).to eq(sample_oid)
- expect(response['objects'].first['size']).to eq(sample_size)
- expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
- expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
- expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
- expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
- end
- end
-
- context 'when pushing a lfs object that does not exist' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it "responds with status 200 and upload hypermedia link" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
-
- response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body['objects']).to be_kind_of(Array)
- expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(response_body['objects'].first['size']).to eq(1575078)
- expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
- expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
- end
- end
-
- context 'when pushing one new and one existing lfs object' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
-
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
-
- response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body['objects']).to be_kind_of(Array)
-
- expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
- expect(response_body['objects'].first['size']).to eq(1575078)
- expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
- expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth)
-
- expect(response_body['objects'].last['oid']).to eq(sample_oid)
- expect(response_body['objects'].last['size']).to eq(sample_size)
- expect(response_body['objects'].last).not_to have_key('actions')
- end
- end
- end
-
- context 'when user does not have push access' do
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'when CI is authorized' do
- it 'responds with 401' do
- expect(lfs_router_ci_auth.try_call.first).to eq(401)
- end
- end
- end
-
- context 'when user is not authenticated' do
- context 'when user has push access' do
- before do
- project.team << [user, :master]
- end
-
- it "responds with status 401" do
- expect(lfs_router_public_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'when user does not have push access' do
- it "responds with status 401" do
- expect(lfs_router_public_noauth.try_call.first).to eq(401)
- end
- end
- end
-
- context 'when CI is authorized' do
- let(:auth) { 'gitlab-ci-token:password' }
-
- before do
- env["HTTP_AUTHORIZATION"] = auth
- end
-
- it "responds with status 403" do
- expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
- end
- end
- end
-
- describe 'unsupported' do
- before do
- body = { 'operation' => 'other',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
-
- it 'responds with status 404' do
- expect(lfs_router_public_noauth.try_call.first).to eq(404)
- end
- end
- end
-
- describe 'when pushing a lfs object' do
- before do
- enable_lfs
- env['REQUEST_METHOD'] = 'PUT'
- end
-
- shared_examples 'unauthorized' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(router.project)
- end
-
- it 'responds with status 401' do
- expect(router.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(router.project)
- end
-
- it 'responds with status 401' do
- expect(router.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent with a malformed headers' do
- before do
- env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
- end
-
- it 'does not recognize it as a valid lfs command' do
- expect(router.try_call).to eq(nil)
- end
- end
- end
-
- shared_examples 'forbidden' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(router.project)
- end
-
- it 'responds with 403' do
- expect(router.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(router.project)
- end
-
- it 'responds with 403' do
- expect(router.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'to one project' do
- describe 'when user is authenticated' do
- describe 'when user has push access to the project' do
- before do
- project.team << [user, :developer]
- end
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with status 200, location of lfs store and object details' do
- json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
-
- expect(lfs_router_auth.try_call.first).to eq(200)
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with status 200 and lfs object is linked to the project' do
- expect(lfs_router_auth.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(project.id)
- end
- end
- end
-
- describe 'and user does not have push access' do
- let(:router) { lfs_router_auth }
-
- it_behaves_like 'forbidden'
- end
- end
-
- context 'when CI is authenticated' do
- let(:router) { lfs_router_ci_auth }
-
- it_behaves_like 'unauthorized'
- end
-
- context 'for unauthenticated' do
- let(:router) { new_lfs_router(project) }
-
- it_behaves_like 'unauthorized'
- end
- end
-
- describe 'to a forked project' do
- let(:forked_project) { fork_project(public_project, user) }
-
- describe 'when user is authenticated' do
- describe 'when user has push access to the project' do
- before do
- forked_project.team << [user_two, :developer]
- end
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with status 200, location of lfs store and object details' do
- json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
-
- expect(lfs_router_forked_auth.try_call.first).to eq(200)
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
- expect(json_response['LfsOid']).to eq(sample_oid)
- expect(json_response['LfsSize']).to eq(sample_size)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
-
- it 'responds with status 200 and lfs object is linked to the source project' do
- expect(lfs_router_forked_auth.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
- end
- end
- end
-
- describe 'and user does not have push access' do
- let(:router) { lfs_router_forked_auth }
-
- it_behaves_like 'forbidden'
- end
- end
-
- context 'when CI is authenticated' do
- let(:router) { lfs_router_forked_ci_auth }
-
- it_behaves_like 'unauthorized'
- end
-
- context 'for unauthenticated' do
- let(:router) { lfs_router_forked_noauth }
-
- it_behaves_like 'unauthorized'
- end
-
- describe 'and second project not related to fork or a source project' do
- let(:second_project) { create(:project) }
- let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
-
- before do
- public_project.lfs_objects << lfs_object
- headers_for_upload_finalize(second_project)
- end
-
- context 'when pushing the same lfs object to the second project' do
- before do
- second_project.team << [user, :master]
- end
-
- it 'responds with 200 and links the lfs object to the project' do
- expect(lfs_router_second_project.try_call.first).to eq(200)
- expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id)
- end
- end
- end
- end
- end
-
- def enable_lfs
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- end
-
- def authorize(user)
- ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
- end
-
- def new_lfs_router(project, user: nil, ci: false)
- Gitlab::Lfs::Router.new(project, user, ci, request)
- end
-
- def header_for_upload_authorize(project)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize"
- end
-
- def headers_for_upload_finalize(project)
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4"
- end
-
- def fork_project(project, user, object = nil)
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
- Projects::ForkService.new(project, user, {}).execute
- end
-end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index 8809b7e3f12..d88bcae41fb 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -39,6 +39,12 @@ describe Gitlab::Metrics::Instrumentation do
allow(@dummy).to receive(:name).and_return('Dummy')
end
+ describe '.series' do
+ it 'returns a String' do
+ expect(described_class.series).to be_an_instance_of(String)
+ end
+ end
+
describe '.configure' do
it 'yields self' do
described_class.configure do |c|
@@ -78,8 +84,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:measure_method).
- with('Dummy.foo')
+ expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure)
@dummy.foo
end
@@ -157,8 +162,7 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
- expect(transaction).to receive(:measure_method).
- with('Dummy#bar')
+ expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure)
@dummy.new.bar
end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index f718d536130..f26fca52c50 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -23,6 +23,24 @@ describe Gitlab::Metrics::Metric do
it { is_expected.to eq({ host: 'localtoast' }) }
end
+ describe '#type' do
+ subject { metric.type }
+
+ it { is_expected.to eq(:metric) }
+ end
+
+ describe '#event?' do
+ it 'returns false for a regular metric' do
+ expect(metric.event?).to eq(false)
+ end
+
+ it 'returns true for an event metric' do
+ expect(metric).to receive(:type).and_return(:event)
+
+ expect(metric.event?).to eq(true)
+ end
+ end
+
describe '#to_hash' do
it 'returns a Hash' do
expect(metric.to_hash).to be_an_instance_of(Hash)
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index f264ed64029..bcaffd27909 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController')
+ klass = double(:klass, name: 'TestController', content_type: 'text/html')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
@@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
- it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ it 'tags a transaction with the method and path of the route in the grape endpoint' do
route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
@@ -45,6 +45,15 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
+
+ it 'tracks any raised exceptions' do
+ expect(app).to receive(:call).with(env).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:rails_exception)
+
+ expect { middleware.call(env) }.to raise_error(RuntimeError)
+ end
end
describe '#transaction_from_env' do
@@ -78,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
+ let(:content_type) { 'text/html' }
- it 'tags a transaction with the name and action of a controller' do
+ before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
env['action_controller.instance'] = controller
+ end
+ it 'tags a transaction with the name and action of a controller' do
middleware.tag_controller(transaction, env)
expect(transaction.action).to eq('TestController#show')
end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
end
describe '#tag_endpoint' do
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 4d2aa03e722..acaba785606 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -12,7 +12,9 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, message, :test) { nil }
@@ -25,10 +27,28 @@ describe Gitlab::Metrics::SidekiqMiddleware do
with('TestWorker#perform').
and_call_original
- expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).with(:sidekiq_queue_duration, instance_of(Float))
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:set).
+ with(:sidekiq_queue_duration, instance_of(Float))
+
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, {}, :test) { nil }
end
+
+ it 'tracks any raised exceptions' do
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:run).and_raise(RuntimeError)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:add_event).with(:sidekiq_exception)
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to receive(:finish)
+
+ expect { middleware.call(worker, message, :test) }.
+ to raise_error(RuntimeError)
+ end
end
end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index cf0e282c2fb..9e2ea89a712 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -28,20 +28,20 @@ describe Gitlab::Metrics::System do
end
describe '.cpu_time' do
- it 'returns a Float' do
- expect(described_class.cpu_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.cpu_time).to be_an_instance_of(Fixnum)
end
end
describe '.real_time' do
- it 'returns a Float' do
- expect(described_class.real_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.real_time).to be_an_instance_of(Fixnum)
end
end
describe '.monotonic_time' do
- it 'returns a Float' do
- expect(described_class.monotonic_time).to be_an_instance_of(Float)
+ it 'returns a Fixnum' do
+ expect(described_class.monotonic_time).to be_an_instance_of(Fixnum)
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 3b1c67a2147..3887c04c832 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -46,19 +46,11 @@ describe Gitlab::Metrics::Transaction do
end
end
- describe '#measure_method' do
- it 'adds a new method if it does not exist already' do
- transaction.measure_method('Foo#bar') { 'foo' }
+ describe '#method_call_for' do
+ it 'returns a MethodCall' do
+ method = transaction.method_call_for('Foo#bar')
- expect(transaction.methods['Foo#bar']).
- to be_an_instance_of(Gitlab::Metrics::MethodCall)
- end
-
- it 'adds timings to an existing method call' do
- transaction.measure_method('Foo#bar') { 'foo' }
- transaction.measure_method('Foo#bar') { 'foo' }
-
- expect(transaction.methods['Foo#bar'].call_count).to eq(2)
+ expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall)
end
end
@@ -150,5 +142,62 @@ describe Gitlab::Metrics::Transaction do
transaction.submit
end
+
+ it 'does not add an action tag for events' do
+ transaction.action = 'Foo#bar'
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: an_instance_of(Fixnum)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(transaction.metrics[0]).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ metric = transaction.metrics[0]
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 96f7eabbca6..ab6e311b1e8 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -147,4 +147,34 @@ describe Gitlab::Metrics do
end
end
end
+
+ describe '#series_prefix' do
+ it 'returns a String' do
+ expect(described_class.series_prefix).to be_an_instance_of(String)
+ end
+ end
+
+ describe '.add_event' do
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ not_to receive(:add_event)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+
+ context 'with a transaction' do
+ it 'adds an event' do
+ transaction = Gitlab::Metrics::Transaction.new
+
+ expect(transaction).to receive(:add_event).with(:meow)
+
+ expect(Gitlab::Metrics).to receive(:current_transaction).
+ and_return(transaction)
+
+ Gitlab::Metrics.add_event(:meow)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index fd6f684db0c..168090d5b5c 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
end
it 'sets proxy_flight_time and calls the app when the header is present' do
- env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+ env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '123'
expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
expect(middleware.call(env)).to eq('yay')
end
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb
deleted file mode 100644
index 3d6bcdfd873..00000000000
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-require 'spec_helper'
-
-describe 'Gitlab::NoteDataBuilder', lib: true do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
- let(:fixed_time) { Time.at(1425600000) } # Avoid time precision errors
-
- before(:each) do
- expect(data).to have_key(:object_attributes)
- expect(data[:object_attributes]).to have_key(:url)
- expect(data[:object_attributes][:url])
- .to eq(Gitlab::UrlBuilder.build(note))
- expect(data[:object_kind]).to eq('note')
- expect(data[:user]).to eq(user.hook_attrs)
- end
-
- describe 'When asking for a note on commit' do
- let(:note) { create(:note_on_commit, project: project) }
-
- it 'returns the note and commit-specific data' do
- expect(data).to have_key(:commit)
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-
- describe 'When asking for a note on commit diff' do
- let(:note) { create(:diff_note_on_commit, project: project) }
-
- it 'returns the note and commit-specific data' do
- expect(data).to have_key(:commit)
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-
- describe 'When asking for a note on issue' do
- let(:issue) do
- create(:issue, created_at: fixed_time, updated_at: fixed_time,
- project: project)
- end
-
- let(:note) do
- create(:note_on_issue, noteable: issue, project: project)
- end
-
- it 'returns the note and issue-specific data' do
- expect(data).to have_key(:issue)
- expect(data[:issue].except('updated_at'))
- .to eq(issue.reload.hook_attrs.except('updated_at'))
- expect(data[:issue]['updated_at'])
- .to be > issue.hook_attrs['updated_at']
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-
- describe 'When asking for a note on merge request' do
- let(:merge_request) do
- create(:merge_request, created_at: fixed_time,
- updated_at: fixed_time,
- source_project: project)
- end
-
- let(:note) do
- create(:note_on_merge_request, noteable: merge_request,
- project: project)
- end
-
- it 'returns the note and merge request data' do
- expect(data).to have_key(:merge_request)
- expect(data[:merge_request].except('updated_at'))
- .to eq(merge_request.reload.hook_attrs.except('updated_at'))
- expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-
- describe 'When asking for a note on merge request diff' do
- let(:merge_request) do
- create(:merge_request, created_at: fixed_time, updated_at: fixed_time,
- source_project: project)
- end
-
- let(:note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: project)
- end
-
- it 'returns the note and merge request diff data' do
- expect(data).to have_key(:merge_request)
- expect(data[:merge_request].except('updated_at'))
- .to eq(merge_request.reload.hook_attrs.except('updated_at'))
- expect(data[:merge_request]['updated_at'])
- .to be > merge_request.hook_attrs['updated_at']
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-
- describe 'When asking for a note on project snippet' do
- let!(:snippet) do
- create(:project_snippet, created_at: fixed_time, updated_at: fixed_time,
- project: project)
- end
-
- let!(:note) do
- create(:note_on_project_snippet, noteable: snippet,
- project: project)
- end
-
- it 'returns the note and project snippet data' do
- expect(data).to have_key(:snippet)
- expect(data[:snippet].except('updated_at'))
- .to eq(snippet.reload.hook_attrs.except('updated_at'))
- expect(data[:snippet]['updated_at'])
- .to be > snippet.hook_attrs['updated_at']
- end
-
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
-end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 1fca8a13037..78c669e8fa5 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::OAuth::User, lib: true do
describe 'signup' do
shared_examples 'to verify compliance with allow_single_sign_on' do
context 'provider is marked as external' do
- it 'should mark user as external' do
+ it 'marks user as external' do
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
oauth_user.save
expect(gl_user).to be_valid
@@ -51,7 +51,7 @@ describe Gitlab::OAuth::User, lib: true do
end
context 'provider was external, now has been removed' do
- it 'should not mark external user as internal' do
+ it 'does not mark external user as internal' do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
oauth_user.save
@@ -62,7 +62,7 @@ describe Gitlab::OAuth::User, lib: true do
context 'provider is not external' do
context 'when adding a new OAuth identity' do
- it 'should not promote an external user to internal' do
+ it 'does not promote an external user to internal' do
user = create(:user, email: 'john@mail.com', external: true)
user.identities.create(provider: provider, extern_uid: uid)
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 270b89972d7..29abb4d4d07 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
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) }
- it 'should not list project confidential issues for non project members' do
+ it 'does not list project confidential issues for non project members' do
results = described_class.new(non_member, project, query)
issues = results.objects('issues')
@@ -43,7 +43,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
- it 'should not list project confidential issues for project members with guest role' do
+ it 'does not list project confidential issues for project members with guest role' do
project.team << [member, :guest]
results = described_class.new(member, project, query)
@@ -55,7 +55,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 1
end
- it 'should list project confidential issues for author' do
+ it 'lists project confidential issues for author' do
results = described_class.new(author, project, query)
issues = results.objects('issues')
@@ -65,7 +65,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 2
end
- it 'should list project confidential issues for assignee' do
+ it 'lists project confidential issues for assignee' do
results = described_class.new(assignee, project.id, query)
issues = results.objects('issues')
@@ -75,7 +75,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 2
end
- it 'should list project confidential issues for project members' do
+ it 'lists project confidential issues for project members' do
project.team << [member, :developer]
results = described_class.new(member, project, query)
@@ -87,7 +87,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
expect(results.issues_count).to eq 3
end
- it 'should list all project issues for admin' do
+ it 'lists all project issues for admin' do
results = described_class.new(admin, project, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
deleted file mode 100644
index 6bd7393aaa7..00000000000
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::PushDataBuilder, lib: true do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- describe '.build_sample' do
- let(:data) { described_class.build_sample(project, user) }
-
- it { expect(data).to be_a(Hash) }
- it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
- it { expect(data[:ref]).to eq('refs/heads/master') }
- it { expect(data[:commits].size).to eq(3) }
- it { expect(data[:total_commits_count]).to eq(3) }
- it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
- it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
- it { expect(data[:commits].first[:removed]).to eq([]) }
-
- include_examples 'project hook data with deprecateds'
- include_examples 'deprecated repository hook data'
- end
-
- describe '.build' do
- let(:data) do
- described_class.build(project, user, Gitlab::Git::BLANK_SHA,
- '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
- 'refs/tags/v1.1.0')
- end
-
- it { expect(data).to be_a(Hash) }
- it { expect(data[:before]).to eq(Gitlab::Git::BLANK_SHA) }
- it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
- it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') }
- it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
- it { expect(data[:user_id]).to eq(user.id) }
- it { expect(data[:user_name]).to eq(user.name) }
- it { expect(data[:user_email]).to eq(user.email) }
- it { expect(data[:user_avatar]).to eq(user.avatar_url) }
- it { expect(data[:project_id]).to eq(project.id) }
- it { expect(data[:project]).to be_a(Hash) }
- it { expect(data[:commits]).to be_empty }
- it { expect(data[:total_commits_count]).to be_zero }
-
- include_examples 'project hook data with deprecateds'
- include_examples 'deprecated repository hook data'
-
- it 'does not raise an error when given nil commits' do
- expect { described_class.build(spy, spy, spy, spy, spy, nil) }.
- not_to raise_error
- end
- end
-end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
new file mode 100644
index 00000000000..e54f5ffb312
--- /dev/null
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::Redis do
+ let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s }
+
+ before(:each) { described_class.reset_params! }
+ after(:each) { described_class.reset_params! }
+
+ describe '.params' do
+ subject { described_class.params }
+
+ context 'when url contains unix socket reference' do
+ let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
+ let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
+
+ context 'with old format' do
+ it 'returns path key instead' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+
+ is_expected.to include(path: '/path/to/old/redis.sock')
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ it 'returns path key instead' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+ is_expected.to include(path: '/path/to/redis.sock')
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+
+ context 'when url is host based' do
+ let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+
+ context 'with old format' do
+ it 'returns hash with host, port, db, and password' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_old }
+
+ is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
+ is_expected.not_to have_key(:url)
+ end
+ end
+
+ context 'with new format' do
+ it 'returns hash with host, port, db, and password' do
+ expect_any_instance_of(described_class).to receive(:config_file) { config_new }
+
+ is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
+ is_expected.not_to have_key(:url)
+ end
+ end
+ end
+ end
+
+ describe '#raw_config_hash' do
+ it 'returns default redis url when no config file is present' do
+ expect(subject).to receive(:fetch_config) { false }
+
+ expect(subject.send(:raw_config_hash)).to eq(url: Gitlab::Redis::DEFAULT_REDIS_URL)
+ end
+
+ it 'returns old-style single url config in a hash' do
+ expect(subject).to receive(:fetch_config) { 'redis://myredis:6379' }
+ expect(subject.send(:raw_config_hash)).to eq(url: 'redis://myredis:6379')
+ end
+ end
+
+ describe '#fetch_config' do
+ it 'returns false when no config file is present' do
+ allow(File).to receive(:exist?).with(redis_config) { false }
+
+ expect(subject.send(:fetch_config)).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb
index 56bf08e7041..02c139f1a0d 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/saml/user_spec.rb
@@ -67,7 +67,7 @@ describe Gitlab::Saml::User, lib: true do
end
context 'user was external, now should not be' do
- it 'should make user internal' do
+ it 'makes user internal' do
existing_user.update_attribute('external', true)
saml_user.save
expect(gl_user).to be_valid
@@ -94,14 +94,14 @@ describe Gitlab::Saml::User, lib: true do
context 'with allow_single_sign_on default (["saml"])' do
before { stub_omniauth_config(allow_single_sign_on: ['saml']) }
- it 'should not throw an error' do
+ it 'does not throw an error' do
expect{ saml_user.save }.not_to raise_error
end
end
context 'with allow_single_sign_on disabled' do
before { stub_omniauth_config(allow_single_sign_on: false) }
- it 'should throw an error' do
+ it 'throws an error' do
expect{ saml_user.save }.to raise_error StandardError
end
end
@@ -223,7 +223,7 @@ describe Gitlab::Saml::User, lib: true do
context 'dont block on create' do
before { stub_omniauth_config(block_auto_created_users: false) }
- it 'should not block the user' do
+ it 'does not block the user' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
@@ -233,7 +233,7 @@ describe Gitlab::Saml::User, lib: true do
context 'block on create' do
before { stub_omniauth_config(block_auto_created_users: true) }
- it 'should block user' do
+ it 'blocks user' do
saml_user.save
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 1bb444bf34f..8a656ab0ee9 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -73,7 +73,7 @@ describe Gitlab::SearchResults do
let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) }
let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') }
- it 'should not list confidential issues for non project members' do
+ it 'does not list confidential issues for non project members' do
results = described_class.new(non_member, limit_projects, query)
issues = results.objects('issues')
@@ -86,7 +86,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
- it 'should not list confidential issues for project members with guest role' do
+ it 'does not list confidential issues for project members with guest role' do
project_1.team << [member, :guest]
project_2.team << [member, :guest]
@@ -102,7 +102,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 1
end
- it 'should list confidential issues for author' do
+ it 'lists confidential issues for author' do
results = described_class.new(author, limit_projects, query)
issues = results.objects('issues')
@@ -115,7 +115,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 3
end
- it 'should list confidential issues for assignee' do
+ it 'lists confidential issues for assignee' do
results = described_class.new(assignee, limit_projects, query)
issues = results.objects('issues')
@@ -128,7 +128,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 3
end
- it 'should list confidential issues for project members' do
+ it 'lists confidential issues for project members' do
project_1.team << [member, :developer]
project_2.team << [member, :developer]
@@ -144,7 +144,7 @@ describe Gitlab::SearchResults do
expect(results.issues_count).to eq 4
end
- it 'should list all issues for admin' do
+ it 'lists all issues for admin' do
results = described_class.new(admin, limit_projects, query)
issues = results.objects('issues')
diff --git a/spec/lib/gitlab/slash_commands/command_definition_spec.rb b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
new file mode 100644
index 00000000000..c9c2f314e57
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/command_definition_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::CommandDefinition do
+ subject { described_class.new(:command) }
+
+ describe "#all_names" do
+ context "when the command has aliases" do
+ before do
+ subject.aliases = [:alias1, :alias2]
+ end
+
+ it "returns an array with the name and aliases" do
+ expect(subject.all_names).to eq([:command, :alias1, :alias2])
+ end
+ end
+
+ context "when the command doesn't have aliases" do
+ it "returns an array with the name" do
+ expect(subject.all_names).to eq([:command])
+ end
+ end
+ end
+
+ describe "#noop?" do
+ context "when the command has an action block" do
+ before do
+ subject.action_block = proc { }
+ end
+
+ it "returns false" do
+ expect(subject.noop?).to be false
+ end
+ end
+
+ context "when the command doesn't have an action block" do
+ it "returns true" do
+ expect(subject.noop?).to be true
+ end
+ end
+ end
+
+ describe "#available?" do
+ let(:opts) { { go: false } }
+
+ context "when the command has a condition block" do
+ before do
+ subject.condition_block = proc { go }
+ end
+
+ context "when the condition block returns true" do
+ before do
+ opts[:go] = true
+ end
+
+ it "returns true" do
+ expect(subject.available?(opts)).to be true
+ end
+ end
+
+ context "when the condition block returns false" do
+ it "returns false" do
+ expect(subject.available?(opts)).to be false
+ end
+ end
+ end
+
+ context "when the command doesn't have a condition block" do
+ it "returns true" do
+ expect(subject.available?(opts)).to be true
+ end
+ end
+ end
+
+ describe "#execute" do
+ let(:context) { OpenStruct.new(run: false) }
+
+ context "when the command is a noop" do
+ it "doesn't execute the command" do
+ expect(context).not_to receive(:instance_exec)
+
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+
+ context "when the command is not a noop" do
+ before do
+ subject.action_block = proc { self.run = true }
+ end
+
+ context "when the command is not available" do
+ before do
+ subject.condition_block = proc { false }
+ end
+
+ it "doesn't execute the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+
+ context "when the command is available" do
+ context "when the commnd has no arguments" do
+ before do
+ subject.action_block = proc { self.run = true }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be true
+ end
+ end
+ end
+
+ context "when the command has 1 required argument" do
+ before do
+ subject.action_block = ->(arg) { self.run = arg }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "doesn't execute the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be false
+ end
+ end
+ end
+
+ context "when the command has 1 optional argument" do
+ before do
+ subject.action_block = proc { |arg = nil| self.run = arg || true }
+ end
+
+ context "when the command is provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, true)
+
+ expect(context.run).to be true
+ end
+ end
+
+ context "when the command is not provided an argument" do
+ it "executes the command" do
+ subject.execute(context, {}, nil)
+
+ expect(context.run).to be true
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/dsl_spec.rb b/spec/lib/gitlab/slash_commands/dsl_spec.rb
new file mode 100644
index 00000000000..26217a0e3b2
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/dsl_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Dsl do
+ before :all do
+ DummyClass = Struct.new(:project) do
+ include Gitlab::SlashCommands::Dsl
+
+ desc 'A command with no args'
+ command :no_args, :none do
+ "Hello World!"
+ end
+
+ params 'The first argument'
+ command :one_arg, :once, :first do |arg1|
+ arg1
+ end
+
+ desc do
+ "A dynamic description for #{noteable.upcase}"
+ end
+ params 'The first argument', 'The second argument'
+ command :two_args do |arg1, arg2|
+ [arg1, arg2]
+ end
+
+ command :cc
+
+ condition do
+ project == 'foo'
+ end
+ command :cond_action do |arg|
+ arg
+ end
+ end
+ end
+
+ describe '.command_definitions' do
+ it 'returns an array with commands definitions' do
+ no_args_def, one_arg_def, two_args_def, cc_def, cond_action_def = DummyClass.command_definitions
+
+ expect(no_args_def.name).to eq(:no_args)
+ expect(no_args_def.aliases).to eq([:none])
+ expect(no_args_def.description).to eq('A command with no args')
+ expect(no_args_def.params).to eq([])
+ expect(no_args_def.condition_block).to be_nil
+ expect(no_args_def.action_block).to be_a_kind_of(Proc)
+
+ expect(one_arg_def.name).to eq(:one_arg)
+ expect(one_arg_def.aliases).to eq([:once, :first])
+ expect(one_arg_def.description).to eq('')
+ expect(one_arg_def.params).to eq(['The first argument'])
+ expect(one_arg_def.condition_block).to be_nil
+ expect(one_arg_def.action_block).to be_a_kind_of(Proc)
+
+ expect(two_args_def.name).to eq(:two_args)
+ expect(two_args_def.aliases).to eq([])
+ expect(two_args_def.to_h(noteable: "issue")[:description]).to eq('A dynamic description for ISSUE')
+ expect(two_args_def.params).to eq(['The first argument', 'The second argument'])
+ expect(two_args_def.condition_block).to be_nil
+ expect(two_args_def.action_block).to be_a_kind_of(Proc)
+
+ expect(cc_def.name).to eq(:cc)
+ expect(cc_def.aliases).to eq([])
+ expect(cc_def.description).to eq('')
+ expect(cc_def.params).to eq([])
+ expect(cc_def.condition_block).to be_nil
+ expect(cc_def.action_block).to be_nil
+
+ expect(cond_action_def.name).to eq(:cond_action)
+ expect(cond_action_def.aliases).to eq([])
+ expect(cond_action_def.description).to eq('')
+ expect(cond_action_def.params).to eq([])
+ expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
+ expect(cond_action_def.action_block).to be_a_kind_of(Proc)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/extractor_spec.rb b/spec/lib/gitlab/slash_commands/extractor_spec.rb
new file mode 100644
index 00000000000..1e4954c4af8
--- /dev/null
+++ b/spec/lib/gitlab/slash_commands/extractor_spec.rb
@@ -0,0 +1,215 @@
+require 'spec_helper'
+
+describe Gitlab::SlashCommands::Extractor do
+ let(:definitions) do
+ Class.new do
+ include Gitlab::SlashCommands::Dsl
+
+ command(:reopen, :open) { }
+ command(:assign) { }
+ command(:labels) { }
+ command(:power) { }
+ end.command_definitions
+ end
+
+ let(:extractor) { described_class.new(definitions) }
+
+ shared_examples 'command with no argument' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['reopen']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ shared_examples 'command with a single argument' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['assign', '@joe']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ shared_examples 'command with multiple arguments' do
+ it 'extracts command' do
+ msg, commands = extractor.extract_commands(original_msg)
+
+ expect(commands).to eq [['labels', '~foo ~"bar baz" label']]
+ expect(msg).to eq final_msg
+ end
+ end
+
+ describe '#extract_commands' do
+ describe 'command with no argument' do
+ context 'at the start of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "/reopen\nworld" }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "hello\n/reopen\nworld" }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = "hello\nworld /reopen"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\nworld /reopen"
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with no argument' do
+ let(:original_msg) { "hello\n/reopen" }
+ let(:final_msg) { "hello" }
+ end
+ end
+ end
+
+ describe 'command with a single argument' do
+ context 'at the start of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "/assign @joe\nworld" }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe\nworld" }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = "hello\nworld /assign @joe"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\nworld /assign @joe"
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with a single argument' do
+ let(:original_msg) { "hello\n/assign @joe" }
+ let(:final_msg) { "hello" }
+ end
+ end
+
+ context 'when argument is not separated with a space' do
+ it 'does not extract command' do
+ msg = "hello\n/assign@joe\nworld"
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq "hello\n/assign@joe\nworld"
+ end
+ end
+ end
+
+ describe 'command with multiple arguments' do
+ context 'at the start of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(/labels ~foo ~"bar baz" label\nworld) }
+ let(:final_msg) { "world" }
+ end
+ end
+
+ context 'in the middle of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label\nworld) }
+ let(:final_msg) { "hello\nworld" }
+ end
+ end
+
+ context 'in the middle of a line' do
+ it 'does not extract command' do
+ msg = %(hello\nworld /labels ~foo ~"bar baz" label)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq %(hello\nworld /labels ~foo ~"bar baz" label)
+ end
+ end
+
+ context 'at the end of content' do
+ it_behaves_like 'command with multiple arguments' do
+ let(:original_msg) { %(hello\n/labels ~foo ~"bar baz" label) }
+ let(:final_msg) { "hello" }
+ end
+ end
+
+ context 'when argument is not separated with a space' do
+ it 'does not extract command' do
+ msg = %(hello\n/labels~foo ~"bar baz" label\nworld)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq %(hello\n/labels~foo ~"bar baz" label\nworld)
+ end
+ end
+ end
+
+ it 'extracts command with multiple arguments and various prefixes' do
+ msg = %(hello\n/power @user.name %9.10 ~"bar baz.2"\nworld)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']]
+ expect(msg).to eq "hello\nworld"
+ end
+
+ it 'extracts multiple commands' do
+ msg = %(hello\n/power @user.name %9.10 ~"bar baz.2" label\nworld\n/reopen)
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2" label'], ['reopen']]
+ expect(msg).to eq "hello\nworld"
+ end
+
+ it 'does not alter original content if no command is found' do
+ msg = 'Fixes #123'
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq 'Fixes #123'
+ end
+
+ it 'does not extract commands inside a blockcode' do
+ msg = "Hello\r\n```\r\nThis is some text\r\n/close\r\n/assign @user\r\n```\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands inside a blockquote' do
+ msg = "Hello\r\n>>>\r\nThis is some text\r\n/close\r\n/assign @user\r\n>>>\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+
+ it 'does not extract commands inside a HTML tag' do
+ msg = "Hello\r\n<div>\r\nThis is some text\r\n/close\r\n/assign @user\r\n</div>\r\n\r\nWorld"
+ expected = msg.delete("\r")
+ msg, commands = extractor.extract_commands(msg)
+
+ expect(commands).to be_empty
+ expect(msg).to eq expected
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb
deleted file mode 100644
index bc0ec9325cc..00000000000
--- a/spec/lib/gitlab/template/gitignore_spec.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Template::Gitignore do
- subject { described_class }
-
- describe '.all' do
- it 'strips the gitignore suffix' do
- expect(subject.all.first.name).not_to end_with('.gitignore')
- end
-
- it 'combines the globals and rest' do
- all = subject.all.map(&:name)
-
- expect(all).to include('Vim')
- expect(all).to include('Ruby')
- end
- end
-
- describe '.find' do
- it 'returns nil if the file does not exist' do
- expect(subject.find('mepmep-yadida')).to be nil
- end
-
- it 'returns the Gitignore object of a valid file' do
- ruby = subject.find('Ruby')
-
- expect(ruby).to be_a Gitlab::Template::Gitignore
- expect(ruby.name).to eq('Ruby')
- end
- end
-
- describe '#content' do
- it 'loads the full file' do
- gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore'))
-
- expect(gitignore.name).to eq 'Ruby'
- expect(gitignore.content).to start_with('*.gem')
- end
- end
-end
diff --git a/spec/lib/gitlab/template/gitignore_template_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb
new file mode 100644
index 00000000000..9750a012e22
--- /dev/null
+++ b/spec/lib/gitlab/template/gitignore_template_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Template::GitignoreTemplate do
+ subject { described_class }
+
+ describe '.all' do
+ it 'strips the gitignore suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitignore')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Vim')
+ expect(all).to include('Ruby')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect(subject.find('mepmep-yadida')).to be nil
+ end
+
+ it 'returns the Gitignore object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Template::GitignoreTemplate
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitignore/Ruby.gitignore'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('*.gem')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
new file mode 100644
index 00000000000..e3b8321eda3
--- /dev/null
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Template::GitlabCiYmlTemplate do
+ subject { described_class }
+
+ describe '.all' do
+ it 'strips the gitlab-ci suffix' do
+ expect(subject.all.first.name).not_to end_with('.gitlab-ci.yml')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all.map(&:name)
+
+ expect(all).to include('Elixir')
+ expect(all).to include('Docker')
+ expect(all).to include('Ruby')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect(subject.find('mepmep-yadida')).to be nil
+ end
+
+ it 'returns the GitlabCiYml object of a valid file' do
+ ruby = subject.find('Ruby')
+
+ expect(ruby).to be_a Gitlab::Template::GitlabCiYmlTemplate
+ expect(ruby.name).to eq('Ruby')
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ gitignore = subject.new(Rails.root.join('vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml'))
+
+ expect(gitignore.name).to eq 'Ruby'
+ expect(gitignore.content).to start_with('#')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
new file mode 100644
index 00000000000..f770857e958
--- /dev/null
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::IssueTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/issue_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/issue_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the issue object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::IssueTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/issue_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/issue_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
new file mode 100644
index 00000000000..bb0f68043fa
--- /dev/null
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Template::MergeRequestTemplate do
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' }
+ let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' }
+ let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
+
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
+ project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
+ project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
+ end
+
+ describe '.all' do
+ it 'strips the md suffix' do
+ expect(subject.all(project).first.name).not_to end_with('.issue_template')
+ end
+
+ it 'combines the globals and rest' do
+ all = subject.all(project).map(&:name)
+
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+ end
+
+ describe '.find' do
+ it 'returns nil if the file does not exist' do
+ expect { subject.find('mepmep-yadida', project) }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ it 'returns the merge request object of a valid file' do
+ ruby = subject.find('bug', project)
+
+ expect(ruby).to be_a Gitlab::Template::MergeRequestTemplate
+ expect(ruby.name).to eq('bug')
+ end
+ end
+
+ describe '.by_category' do
+ it 'return array of templates' do
+ all = subject.by_category('', project).map(&:name)
+ expect(all).to include('bug')
+ expect(all).to include('feature_proposal')
+ expect(all).to include('template_test')
+ end
+
+ context 'when repo is bare or empty' do
+ let(:empty_project) { create(:empty_project) }
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "returns empty array" do
+ templates = subject.by_category('', empty_project)
+ expect(templates).to be_empty
+ end
+ end
+ end
+
+ describe '#content' do
+ it 'loads the full file' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bug.md', project)
+
+ expect(issue_template.name).to eq 'bug'
+ expect(issue_template.content).to eq('something valid')
+ end
+
+ it 'raises error when file is not found' do
+ issue_template = subject.new('.gitlab/merge_request_templates/bugnot.md', project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+
+ context "when repo is empty" do
+ let(:empty_project) { create(:empty_project) }
+
+ before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+
+ it "raises file not found" do
+ issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
+ expect { issue_template.content }.to raise_error(Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index e958e087a80..edadab043d7 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -9,19 +9,19 @@ describe Gitlab::Upgrader, lib: true do
end
describe 'latest_version?' do
- it 'should be true if newest version' do
+ it 'is true if newest version' do
allow(upgrader).to receive(:latest_version_raw).and_return(current_version)
expect(upgrader.latest_version?).to be_truthy
end
end
describe 'latest_version_raw' do
- it 'should be latest version for GitLab 5' do
+ it 'is the latest version for GitLab 5' do
allow(upgrader).to receive(:current_version_raw).and_return("5.3.0")
expect(upgrader.latest_version_raw).to eq("v5.4.2")
end
- it 'should get the latest version from tags' do
+ it 'gets the latest version from tags' do
allow(upgrader).to receive(:fetch_git_tags).and_return([
'6f0733310546402c15d3ae6128a95052f6c8ea96 refs/tags/v7.1.1',
'facfec4b242ce151af224e20715d58e628aa5e74 refs/tags/v7.1.1^{}',
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
new file mode 100644
index 00000000000..d3c3b800b94
--- /dev/null
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+describe Gitlab::UserAccess, lib: true do
+ let(:access) { Gitlab::UserAccess.new(user, project: project) }
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ describe 'can_push_to_branch?' do
+ describe 'push to none protected branch' do
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?('random_branch')).to be_falsey
+ end
+ end
+
+ describe 'push to empty project' do
+ let(:empty_project) { create(:project_empty_repo) }
+ let(:project_access) { Gitlab::UserAccess.new(user, project: empty_project) }
+
+ it 'returns true if user is master' do
+ empty_project.team << [user, :master]
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+
+ it 'returns false if user is developer and project is fully protected' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(project_access.can_push_to_branch?('master')).to be_falsey
+ end
+
+ it 'returns false if user is developer and it is not allowed to push new commits but can merge into branch' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project_access.can_push_to_branch?('master')).to be_falsey
+ end
+
+ it 'returns true if user is developer and project is unprotected' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+
+ it 'returns true if user is developer and project grants developers permission' do
+ empty_project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(project_access.can_push_to_branch?('master')).to be_truthy
+ end
+ end
+
+ describe 'push to protected branch' do
+ let(:branch) { create :protected_branch, project: project }
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_falsey
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?(branch.name)).to be_falsey
+ end
+ end
+
+ describe 'push to protected branch if allowed for developers' do
+ before do
+ @branch = create :protected_branch, :developers_can_push, project: project
+ end
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_push_to_branch?(@branch.name)).to be_falsey
+ end
+ end
+
+ describe 'merge to protected branch if allowed for developers' do
+ before do
+ @branch = create :protected_branch, :developers_can_merge, project: project
+ end
+
+ it 'returns true if user is a master' do
+ project.team << [user, :master]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.team << [user, :developer]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.team << [user, :reporter]
+
+ expect(access.can_merge_to_branch?(@branch.name)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb
index 63b5292b098..f227926f39c 100644
--- a/spec/lib/repository_cache_spec.rb
+++ b/spec/lib/repository_cache_spec.rb
@@ -1,33 +1,34 @@
-require_relative '../../lib/repository_cache'
+require 'spec_helper'
describe RepositoryCache, lib: true do
+ let(:project) { create(:project) }
let(:backend) { double('backend').as_null_object }
- let(:cache) { RepositoryCache.new('example', backend) }
+ let(:cache) { RepositoryCache.new('example', project.id, backend) }
describe '#cache_key' do
it 'includes the namespace' do
- expect(cache.cache_key(:foo)).to eq 'foo:example'
+ expect(cache.cache_key(:foo)).to eq "foo:example:#{project.id}"
end
end
describe '#expire' do
it 'expires the given key from the cache' do
cache.expire(:foo)
- expect(backend).to have_received(:delete).with('foo:example')
+ expect(backend).to have_received(:delete).with("foo:example:#{project.id}")
end
end
describe '#fetch' do
it 'fetches the given key from the cache' do
cache.fetch(:bar)
- expect(backend).to have_received(:fetch).with('bar:example')
+ expect(backend).to have_received(:fetch).with("bar:example:#{project.id}")
end
it 'accepts a block' do
p = -> {}
cache.fetch(:baz, &p)
- expect(backend).to have_received(:fetch).with('baz:example', &p)
+ expect(backend).to have_received(:fetch).with("baz:example:#{project.id}", &p)
end
end
end
diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb
new file mode 100644
index 00000000000..4d3811af254
--- /dev/null
+++ b/spec/mailers/emails/merge_requests_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+require 'email_spec'
+require 'mailers/shared/notify'
+
+describe Notify, "merge request notifications" do
+ include EmailSpec::Matchers
+
+ describe "#resolved_all_discussions_email" do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+ let(:current_user) { create(:user) }
+
+ subject { Notify.resolved_all_discussions_email(user.id, merge_request.id, current_user.id) }
+
+ it "includes the name of the resolver" do
+ expect(subject).to have_body_text current_user.name
+ end
+ end
+end
diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb
index c6758ccad39..781472d0c00 100644
--- a/spec/mailers/emails/profile_spec.rb
+++ b/spec/mailers/emails/profile_spec.rb
@@ -48,7 +48,7 @@ describe Notify do
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
- it 'should not contain the new user\'s password' do
+ it 'does not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0a9b10bebea..eae9c060c38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -12,7 +12,7 @@ describe Notify do
context 'for a project' do
describe 'items that are assignable, the email' do
let(:current_user) { create(:user, email: "current@email.com") }
- let(:assignee) { create(:user, email: 'assignee@example.com') }
+ let(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
shared_examples 'an assignee email' do
@@ -493,7 +493,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
- ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ project.project_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
project.project_members.invite.last
end
@@ -591,7 +596,7 @@ describe Notify do
is_expected.to have_body_text /#{note.note}/
end
- it 'not contains note author' do
+ it 'does not contain note author' do
is_expected.not_to have_body_text /wrote\:/
end
@@ -740,7 +745,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
- GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ group.group_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
group.group_members.invite.last
end
@@ -944,8 +954,9 @@ describe Notify do
describe 'email on push with multiple commits' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
- let(:commits) { Commit.decorate(compare.commits, nil) }
+ let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
+ let(:compare) { Compare.decorate(raw_compare, project) }
+ let(:commits) { compare.commits }
let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false }
let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
@@ -1046,8 +1057,9 @@ describe Notify do
describe 'email on push with a single commit' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
- let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
- let(:commits) { Commit.decorate(compare.commits, nil) }
+ let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
+ let(:compare) { Compare.decorate(raw_compare, project) }
+ let(:commits) { compare.commits }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
let(:diff_refs) { Gitlab::Diff::DiffRefs.new(base_sha: project.merge_base_commit(sample_image_commit.id, sample_commit.id).id, head_sha: sample_commit.id) }
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 1acb5846fcf..aa3b2bbf471 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,62 @@
require 'spec_helper'
describe Ability, lib: true do
+ describe '.can_edit_note?' do
+ let(:project) { create(:empty_project) }
+ let!(:note) { create(:note_on_issue, project: project) }
+
+ context 'using an anonymous user' do
+ it 'returns false' do
+ expect(described_class.can_edit_note?(nil, note)).to be_falsy
+ end
+ end
+
+ context 'using a system note' do
+ it 'returns false' do
+ system_note = create(:note, system: true)
+ user = create(:user)
+
+ expect(described_class.can_edit_note?(user, system_note)).to be_falsy
+ end
+ end
+
+ context 'using users with different access levels' do
+ let(:user) { create(:user) }
+
+ it 'returns true for the author' do
+ expect(described_class.can_edit_note?(note.author, note)).to be_truthy
+ end
+
+ it 'returns false for a guest user' do
+ project.team << [user, :guest]
+
+ expect(described_class.can_edit_note?(user, note)).to be_falsy
+ end
+
+ it 'returns false for a developer' do
+ project.team << [user, :developer]
+
+ expect(described_class.can_edit_note?(user, note)).to be_falsy
+ end
+
+ it 'returns true for a master' do
+ project.team << [user, :master]
+
+ expect(described_class.can_edit_note?(user, note)).to be_truthy
+ end
+
+ it 'returns true for a group owner' do
+ group = create(:group)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::MASTER)
+ group.add_owner(user)
+
+ expect(described_class.can_edit_note?(user, note)).to be_truthy
+ end
+ end
+ end
+
describe '.users_that_can_read_project' do
context 'using a public project' do
it 'returns all the users' do
@@ -114,4 +170,116 @@ describe Ability, lib: true do
end
end
end
+
+ shared_examples_for ".project_abilities" do |enable_request_store|
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ describe '.project_abilities' do
+ let!(:project) { create(:empty_project, :public) }
+ let!(:user) { create(:user) }
+
+ it 'returns permissions for admin user' do
+ admin = create(:admin)
+
+ results = described_class.project_abilities(admin, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for an owner' do
+ results = described_class.project_abilities(project.owner, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for a master' do
+ project.team << [user, :master]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(60)
+ end
+
+ it 'returns permissions for a developer' do
+ project.team << [user, :developer]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(44)
+ end
+
+ it 'returns permissions for a guest' do
+ project.team << [user, :guest]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(21)
+ end
+ end
+ end
+
+ describe '.project_abilities with RequestStore' do
+ it_behaves_like ".project_abilities", true
+ end
+
+ describe '.project_abilities without RequestStore' do
+ it_behaves_like ".project_abilities", false
+ end
+
+ describe '.issues_readable_by_user' do
+ context 'with an admin user' do
+ it 'returns all given issues' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(described_class.issues_readable_by_user([issue], user)).
+ to eq([issue])
+ end
+ end
+
+ context 'with a regular user' do
+ it 'returns the issues readable by the user' do
+ user = build(:user)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(described_class.issues_readable_by_user([issue], user)).
+ to eq([issue])
+ end
+
+ it 'returns an empty Array when no issues are readable' do
+ user = build(:user)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(false)
+
+ expect(described_class.issues_readable_by_user([issue], user)).to eq([])
+ end
+ end
+
+ context 'without a regular user' do
+ it 'returns issues that are publicly visible' do
+ hidden_issue = build(:issue)
+ visible_issue = build(:issue)
+
+ expect(hidden_issue).to receive(:publicly_visible?).and_return(false)
+ expect(visible_issue).to receive(:publicly_visible?).and_return(true)
+
+ issues = described_class.
+ issues_readable_by_user([hidden_issue, visible_issue])
+
+ expect(issues).to eq([visible_issue])
+ end
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 2ea1320267c..cc215d252f9 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -53,24 +53,61 @@ describe ApplicationSetting, models: true do
end
context 'restricted signup domains' do
- it 'set single domain' do
- setting.restricted_signup_domains_raw = 'example.com'
- expect(setting.restricted_signup_domains).to eq(['example.com'])
+ it 'sets single domain' do
+ setting.domain_whitelist_raw = 'example.com'
+ expect(setting.domain_whitelist).to eq(['example.com'])
end
- it 'set multiple domains with spaces' do
- setting.restricted_signup_domains_raw = 'example.com *.example.com'
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with spaces' do
+ setting.domain_whitelist_raw = 'example.com *.example.com'
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
end
- it 'set multiple domains with newlines and a space' do
- setting.restricted_signup_domains_raw = "example.com\n *.example.com"
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_whitelist_raw = "example.com\n *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
end
- it 'set multiple domains with commas' do
- setting.restricted_signup_domains_raw = "example.com, *.example.com"
- expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ it 'sets multiple domains with commas' do
+ setting.domain_whitelist_raw = "example.com, *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
+ end
+ end
+
+ context 'blacklisted signup domains' do
+ it 'sets single domain' do
+ setting.domain_blacklist_raw = 'example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com')
+ end
+
+ it 'sets multiple domains with spaces' do
+ setting.domain_blacklist_raw = 'example.com *.example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_blacklist_raw = "example.com\n *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with commas' do
+ setting.domain_blacklist_raw = "example.com, *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with semicolon' do
+ setting.domain_blacklist_raw = "example.com; *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with mixture of everything' do
+ setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
+ end
+
+ it 'sets multiple domain with file' do
+ setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
+ expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 78e95c8fac5..cee20234e1f 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -33,6 +33,22 @@ describe Blob do
end
end
+ describe '#video?' do
+ it 'is falsey with image extension' do
+ git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+
+ expect(described_class.decorate(git_blob)).not_to be_video
+ end
+
+ UploaderHelper::VIDEO_EXT.each do |ext|
+ it "is truthy when extension is .#{ext}" do
+ git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
+
+ expect(described_class.decorate(git_blob)).to be_video
+ end
+ end
+ end
+
describe '#to_partial_path' do
def stubbed_blob(overrides = {})
overrides.reverse_merge!(
@@ -78,4 +94,26 @@ describe Blob do
expect(blob.to_partial_path).to eq 'download'
end
end
+
+ describe '#size_within_svg_limits?' do
+ let(:blob) { described_class.decorate(double(:blob)) }
+
+ it 'returns true when the blob size is smaller than the SVG limit' do
+ expect(blob).to receive(:size).and_return(42)
+
+ expect(blob.size_within_svg_limits?).to eq(true)
+ end
+
+ it 'returns true when the blob size is equal to the SVG limit' do
+ expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+
+ expect(blob.size_within_svg_limits?).to eq(true)
+ end
+
+ it 'returns false when the blob size is larger than the SVG limit' do
+ expect(blob).to receive(:size).and_return(1.terabyte)
+
+ expect(blob.size_within_svg_limits?).to eq(false)
+ end
+ end
end
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
new file mode 100644
index 00000000000..12d29540137
--- /dev/null
+++ b/spec/models/board_spec.rb
@@ -0,0 +1,12 @@
+require 'rails_helper'
+
+describe Board do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 6ad8bfef4f2..02d6263094a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- include ActiveSupport::Testing::TimeHelpers
-
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
@@ -23,19 +21,19 @@ describe BroadcastMessage, models: true do
end
describe '.current' do
- it "should return last message if time match" do
+ it "returns last message if time match" do
message = create(:broadcast_message)
expect(BroadcastMessage.current).to eq message
end
- it "should return nil if time not come" do
+ it "returns nil if time not come" do
create(:broadcast_message, :future)
expect(BroadcastMessage.current).to be_nil
end
- it "should return nil if time has passed" do
+ it "returns nil if time has passed" do
create(:broadcast_message, :expired)
expect(BroadcastMessage.current).to be_nil
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e8171788872..ee2c3d04984 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -5,7 +5,9 @@ describe Ci::Build, models: true do
let(:pipeline) do
create(:ci_pipeline, project: project,
- sha: project.commit.id)
+ sha: project.commit.id,
+ ref: project.default_branch,
+ status: 'success')
end
let(:build) { create(:ci_build, pipeline: pipeline) }
@@ -30,7 +32,7 @@ describe Ci::Build, models: true do
end
let(:create_from_build) { Ci::Build.create_from build }
- it 'there should be a pending task' do
+ it 'exists a pending task' do
expect(Ci::Build.pending.count(:all)).to eq 0
create_from_build
expect(Ci::Build.pending.count(:all)).to be > 0
@@ -40,7 +42,7 @@ describe Ci::Build, models: true do
describe '#ignored?' do
subject { build.ignored? }
- context 'if build is not allowed to fail' do
+ context 'when build is not allowed to fail' do
before do
build.allow_failure = false
end
@@ -62,7 +64,7 @@ describe Ci::Build, models: true do
end
end
- context 'if build is allowed to fail' do
+ context 'when build is allowed to fail' do
before do
build.allow_failure = true
end
@@ -90,7 +92,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_empty }
- context 'if build.trace contains text' do
+ context 'when build.trace contains text' do
let(:text) { 'example output' }
before do
build.trace = text
@@ -100,7 +102,7 @@ describe Ci::Build, models: true do
it { expect(subject.length).to be >= text.length }
end
- context 'if build.trace hides token' do
+ context 'when build.trace hides token' do
let(:token) { 'my_secret_token' }
before do
@@ -191,93 +193,185 @@ describe Ci::Build, models: true do
end
describe '#variables' do
+ let(:container_registry_enabled) { false }
+ let(:predefined_variables) do
+ [
+ { key: 'CI', value: 'true', public: true },
+ { key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+ { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+ { key: 'CI_BUILD_REF', value: build.sha, public: true },
+ { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
+ { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true },
+ { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ ]
+ end
+
+ before do
+ stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com')
+ end
+
+ subject { build.variables }
+
context 'returns variables' do
- subject { build.variables }
+ before do
+ build.yaml_variables = []
+ end
- let(:predefined_variables) do
- [
- { key: :CI_BUILD_NAME, value: 'test', public: true },
- { key: :CI_BUILD_STAGE, value: 'stage', public: true },
- ]
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when build is for tag' do
+ let(:tag_variable) do
+ { key: 'CI_BUILD_TAG', value: 'master', public: true }
end
- let(:yaml_variables) do
- [
- { key: :DB_NAME, value: 'postgres', public: true }
- ]
+ before do
+ build.update_attributes(tag: true)
+ end
+
+ it { is_expected.to include(tag_variable) }
+ end
+
+ context 'when secure variable is defined' do
+ let(:secure_variable) do
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
- build.update_attributes(stage: 'stage')
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end
- it { is_expected.to eq(predefined_variables + yaml_variables) }
+ it { is_expected.to include(secure_variable) }
+ end
- context 'for tag' do
- let(:tag_variable) do
- [
- { key: :CI_BUILD_TAG, value: 'master', public: true }
- ]
- end
+ context 'when build is for triggers' do
+ let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
+ let(:user_trigger_variable) do
+ { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
+ end
+ let(:predefined_trigger_variable) do
+ { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ end
- before do
- build.update_attributes(tag: true)
- end
+ before do
+ build.trigger_request = trigger_request
+ end
- it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
+ it { is_expected.to include(user_trigger_variable) }
+ it { is_expected.to include(predefined_trigger_variable) }
+ end
+
+ context 'when yaml_variables are undefined' do
+ before do
+ build.yaml_variables = nil
end
- context 'and secure variables' do
- let(:secure_variables) do
- [
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
- ]
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
end
- before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq(predefined_variables) }
end
- it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) }
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
- context 'and trigger variables' do
- let(:trigger) { create(:ci_trigger, project: project) }
- let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) }
- let(:trigger_variables) do
- [
- { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
- ]
+ it { is_expected.to eq(predefined_variables) }
+ end
+
+ context 'when config has variables' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ variables: {
+ KEY: 'value'
+ }
+ }
+ })
end
- let(:predefined_trigger_variable) do
- [
- { key: :CI_BUILD_TRIGGERED, value: 'true', public: true }
- ]
+ let(:variables) do
+ [{ key: :KEY, value: 'value', public: true }]
end
- before do
- build.trigger_request = trigger_request
- end
+ it { is_expected.to eq(predefined_variables + variables) }
+ end
+ end
+ end
- it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) }
+ context 'when container registry is enabled' do
+ let(:container_registry_enabled) { true }
+ let(:ci_registry) do
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ end
+ let(:ci_registry_image) do
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ end
+
+ context 'and is disabled for project' do
+ before do
+ project.update(container_registry_enabled: false)
end
- context 'when job variables are defined' do
- ##
- # Job-level variables are defined in gitlab_ci.yml fixture
- #
- context 'when job variables are unique' do
- let(:build) { create(:ci_build, name: 'staging') }
-
- it 'includes job variables' do
- expect(subject).to include(
- { key: :KEY1, value: 'value1', public: true },
- { key: :KEY2, value: 'value2', public: true }
- )
- end
- end
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.not_to include(ci_registry_image) }
+ end
+
+ context 'and is enabled for project' do
+ before do
+ project.update(container_registry_enabled: true)
end
+
+ it { is_expected.to include(ci_registry) }
+ it { is_expected.to include(ci_registry_image) }
end
end
+
+ context 'when runner is assigned to build' do
+ let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) }
+
+ before do
+ build.update(runner: runner)
+ end
+
+ it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
+ end
+
+ context 'returns variables in valid order' do
+ before do
+ allow(build).to receive(:predefined_variables) { ['predefined'] }
+ allow(project).to receive(:predefined_variables) { ['project'] }
+ allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
+ allow(build).to receive(:yaml_variables) { ['yaml'] }
+ allow(project).to receive(:secret_variables) { ['secret'] }
+ end
+
+ it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ end
end
describe '#has_tags?' do
@@ -299,7 +393,7 @@ describe Ci::Build, models: true do
it { is_expected.to be_falsey }
end
- context 'if there are runner' do
+ context 'when there are runners' do
let(:runner) { create(:ci_runner) }
before do
@@ -329,29 +423,27 @@ describe Ci::Build, models: true do
describe '#stuck?' do
subject { build.stuck? }
- %w(pending).each do |state|
- context "if commit_status.status is #{state}" do
- before do
- build.status = state
- end
-
- it { is_expected.to be_truthy }
+ context "when commit_status.status is pending" do
+ before do
+ build.status = 'pending'
+ end
- context "and there are specific runner" do
- let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
+ it { is_expected.to be_truthy }
- before do
- build.project.runners << runner
- runner.save
- end
+ context "and there are specific runner" do
+ let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) }
- it { is_expected.to be_falsey }
+ before do
+ build.project.runners << runner
+ runner.save
end
+
+ it { is_expected.to be_falsey }
end
end
- %w(success failed canceled running).each do |state|
- context "if commit_status.status is #{state}" do
+ %w[success failed canceled running].each do |state|
+ context "when commit_status.status is #{state}" do
before do
build.status = state
end
@@ -479,19 +571,19 @@ describe Ci::Build, models: true do
let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') }
let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') }
- it 'to have no dependents if this is first build' do
+ it 'expects to have no dependents if this is first build' do
expect(build.depends_on_builds).to be_empty
end
- it 'to have one dependent if this is test' do
+ it 'expects to have one dependent if this is test' do
expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id)
end
- it 'to have all builds from build and test stage if this is last' do
+ it 'expects to have all builds from build and test stage if this is last' do
expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id)
end
- it 'to have retried builds instead the original ones' do
+ it 'expects to have retried builds instead the original ones' do
retried_rspec = Ci::Build.retry(rspec_test)
expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id)
end
@@ -561,23 +653,23 @@ describe Ci::Build, models: true do
describe 'build erasable' do
shared_examples 'erasable' do
- it 'should remove artifact file' do
+ it 'removes artifact file' do
expect(build.artifacts_file.exists?).to be_falsy
end
- it 'should remove artifact metadata file' do
+ it 'removes artifact metadata file' do
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'should erase build trace in trace file' do
+ it 'erases build trace in trace file' do
expect(build.trace).to be_empty
end
- it 'should set erased to true' do
+ it 'sets erased to true' do
expect(build.erased?).to be true
end
- it 'should set erase date' do
+ it 'sets erase date' do
expect(build.erased_at).not_to be_falsy
end
end
@@ -610,7 +702,7 @@ describe Ci::Build, models: true do
include_examples 'erasable'
- it 'should record user who erased a build' do
+ it 'records user who erased a build' do
expect(build.erased_by).to eq user
end
end
@@ -620,7 +712,7 @@ describe Ci::Build, models: true do
include_examples 'erasable'
- it 'should not set user who erased a build' do
+ it 'does not set user who erased a build' do
expect(build.erased_by).to be_nil
end
end
@@ -628,7 +720,7 @@ describe Ci::Build, models: true do
describe '#erasable?' do
subject { build.erasable? }
- it { is_expected.to eq true }
+ it { is_expected.to be_truthy }
end
describe '#erased?' do
@@ -636,7 +728,7 @@ describe Ci::Build, models: true do
subject { build.erased? }
context 'build has not been erased' do
- it { is_expected.to be false }
+ it { is_expected.to be_falsey }
end
context 'build has been erased' do
@@ -644,18 +736,19 @@ describe Ci::Build, models: true do
build.erase
end
- it { is_expected.to be true }
+ it { is_expected.to be_truthy }
end
end
context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) }
+
before do
build.remove_artifacts_metadata!
end
describe '#erase' do
- it 'should not raise error' do
+ it 'does not raise error' do
expect { build.erase }.not_to raise_error
end
end
@@ -669,11 +762,192 @@ describe Ci::Build, models: true do
end
end
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has `when`' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
+ describe '#retryable?' do
+ context 'when build is running' do
+ before do
+ build.run!
+ end
+
+ it { expect(build).not_to be_retryable }
+ end
+
+ context 'when build is finished' do
+ before do
+ build.success!
+ end
+
+ it { expect(build).to be_retryable }
+ end
+ end
+
+ describe '#manual?' do
+ before do
+ build.update(when: value)
+ end
+
+ subject { build.manual? }
+
+ context 'when is set to manual' do
+ let(:value) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when set to something else' do
+ let(:value) { 'something else' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#other_actions' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+ let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') }
+
+ subject { build.other_actions }
+
+ it 'returns other actions' do
+ is_expected.to contain_exactly(other_build)
+ end
+
+ context 'when build is retried' do
+ let!(:new_build) { Ci::Build.retry(build) }
+
+ it 'does not return any of them' do
+ is_expected.not_to include(build, new_build)
+ end
+ end
+
+ context 'when other build is retried' do
+ let!(:retried_build) { Ci::Build.retry(other_build) }
+
+ it 'returns a retried build' do
+ is_expected.to contain_exactly(retried_build)
+ end
+ end
+ end
+
+ describe '#play' do
+ let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ subject { build.play }
+
+ it 'enqueues a build' do
+ is_expected.to be_pending
+ is_expected.to eq(build)
+ end
+
+ context 'for successful build' do
+ before do
+ build.update(status: 'success')
+ end
+
+ it 'creates a new build' do
+ is_expected.to be_pending
+ is_expected.not_to eq(build)
+ end
+ end
+ end
+
+ describe '#when' do
+ subject { build.when }
+
+ context 'when `when` is undefined' do
+ before do
+ build.when = nil
+ end
+
+ context 'use from gitlab-ci.yml' do
+ before do
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ context 'when config is not found' do
+ let(:config) { nil }
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config does not have a questioned job' do
+ let(:config) do
+ YAML.dump({
+ test_other: {
+ script: 'Hello World'
+ }
+ })
+ end
+
+ it { is_expected.to eq('on_success') }
+ end
+
+ context 'when config has when' do
+ let(:config) do
+ YAML.dump({
+ test: {
+ script: 'Hello World',
+ when: 'always'
+ }
+ })
+ end
+
+ it { is_expected.to eq('always') }
+ end
+ end
+ end
+ end
+
describe '#retryable?' do
context 'when build is running' do
before { build.run! }
- it 'should return false' do
+ it 'returns false' do
expect(build.retryable?).to be false
end
end
@@ -681,7 +955,7 @@ describe Ci::Build, models: true do
context 'when build is finished' do
before { build.success! }
- it 'should return true' do
+ it 'returns true' do
expect(build.retryable?).to be true
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 34507cf5083..721b20e0cb2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2,12 +2,15 @@ require 'spec_helper'
describe Ci::Pipeline, models: true do
let(:project) { FactoryGirl.create :empty_project }
- let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, status: 'created', project: project }
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:user) }
+
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:builds) }
+
it { is_expected.to validate_presence_of :sha }
it { is_expected.to validate_presence_of :status }
@@ -15,7 +18,9 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
- describe :valid_commit_sha do
+ 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
pipeline.sha = '0' * 40
@@ -26,7 +31,7 @@ describe Ci::Pipeline, models: true do
end
end
- describe :short_sha do
+ describe '#short_sha' do
subject { pipeline.short_sha }
it 'has 8 items' do
@@ -35,10 +40,7 @@ describe Ci::Pipeline, models: true do
it { expect(pipeline.sha).to start_with(subject) }
end
- describe :create_next_builds do
- end
-
- describe :retried do
+ describe '#retried' do
subject { pipeline.retried }
before do
@@ -51,366 +53,350 @@ describe Ci::Pipeline, models: true do
end
end
- describe :create_builds do
- let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project, ref: 'master', tag: false }
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
+ let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
- def create_builds(trigger_request = nil)
- pipeline.create_builds(nil, trigger_request)
+ it "calculates average when there are two builds with coverage" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
end
- def create_next_builds
- pipeline.create_next_builds(pipeline.builds.order(:id).last)
+ it "calculates average when there are two builds with coverage and one with nil" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
end
- it 'creates builds' do
- expect(create_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(2)
+ it "calculates average when there are two builds with coverage and one is retried" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ expect(pipeline.coverage).to eq("35.00")
+ end
- expect(create_next_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(4)
+ it "calculates average when there is one build without coverage" do
+ FactoryGirl.create :ci_build, pipeline: pipeline
+ expect(pipeline.coverage).to be_nil
+ end
+ end
- expect(create_next_builds).to be_truthy
- pipeline.builds.update_all(status: "success")
- expect(pipeline.builds.count(:all)).to eq(5)
+ describe '#retryable?' do
+ subject { pipeline.retryable? }
- expect(create_next_builds).to be_falsey
- end
+ context 'no failed builds' do
+ before do
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ end
- context 'custom stage with first job allowed to fail' do
- let(:yaml) do
- {
- stages: ['clean', 'test'],
- clean_job: {
- stage: 'clean',
- allow_failure: true,
- script: 'BUILD',
- },
- test_job: {
- stage: 'test',
- script: 'TEST',
- },
- }
+ it 'be not retryable' do
+ is_expected.to be_falsey
end
+ end
+ context 'with failed builds' do
before do
- stub_ci_pipeline_yaml_file(YAML.dump(yaml))
- create_builds
- end
-
- it 'properly schedules builds' do
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:drop)
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending', 'failed')
- end
- end
-
- context 'properly creates builds when "when" is defined' do
- let(:yaml) do
- {
- stages: ["build", "test", "test_failure", "deploy", "cleanup"],
- build: {
- stage: "build",
- script: "BUILD",
- },
- test: {
- stage: "test",
- script: "TEST",
- },
- test_failure: {
- stage: "test_failure",
- script: "ON test failure",
- when: "on_failure",
- },
- deploy: {
- stage: "deploy",
- script: "PUBLISH",
- },
- cleanup: {
- stage: "cleanup",
- script: "TIDY UP",
- when: "always",
- }
- }
+ FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
+ FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
end
- before do
- stub_ci_pipeline_yaml_file(YAML.dump(yaml))
+ it 'be retryable' do
+ is_expected.to be_truthy
end
+ end
+ end
- context 'when builds are successful' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ describe '#stages' do
+ let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
+ subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ before do
+ FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
+ FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ end
+
+ it 'return all stages' do
+ is_expected.to eq(%w(build test))
+ end
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ describe 'state machine' do
+ let(:current) { Time.now.change(usec: 0) }
+ let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ describe '#duration' do
+ before do
+ travel_to(current - 120) do
+ pipeline.run
+ end
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('success')
+ travel_to(current) do
+ pipeline.succeed
end
end
- context 'when test job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ it 'matches sum of builds duration' do
+ expect(pipeline.reload.duration).to eq(120)
+ end
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
+ describe '#started_at' do
+ it 'updates on transitioning to running' do
+ build.run
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ expect(pipeline.reload.started_at).not_to be_nil
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ it 'does not update on transitioning to success' do
+ build.success
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ expect(pipeline.reload.started_at).to be_nil
end
+ end
- context 'when test and test_failure jobs fail' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
+ describe '#finished_at' do
+ it 'updates on transitioning to success' do
+ build.success
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
+ expect(pipeline.reload.finished_at).not_to be_nil
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ it 'does not update on transitioning to running' do
+ build.run
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ expect(pipeline.reload.finished_at).to be_nil
end
+ end
+ end
- context 'when deploy job fails' do
- it 'properly creates builds' do
- expect(create_builds).to be_truthy
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ describe '#branch?' do
+ subject { pipeline.branch? }
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ context 'is not a tag' do
+ before do
+ pipeline.tag = false
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
+ it 'return true when tag is set to false' do
+ is_expected.to be_truthy
+ end
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
+ context 'is not a tag' do
+ before do
+ pipeline.tag = true
+ end
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ it 'return false when tag is set to true' do
+ is_expected.to be_falsey
end
+ end
+ end
- context 'when build is canceled in the second stage' do
- it 'does not schedule builds after build has been canceled' do
- expect(create_builds).to be_truthy
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ describe '#manual_actions' do
+ subject { pipeline.manual_actions }
- expect(pipeline.builds.running_or_pending).not_to be_empty
+ it 'when none defined' do
+ is_expected.to be_empty
+ end
- expect(pipeline.builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(pipeline.builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:cancel)
+ context 'when action defined' do
+ let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
- expect(pipeline.builds.running_or_pending).to be_empty
- expect(pipeline.reload.status).to eq('canceled')
+ it 'returns one action' do
+ is_expected.to contain_exactly(manual)
+ end
+
+ context 'there are multiple of the same name' do
+ let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
+
+ it 'returns latest one' do
+ is_expected.to contain_exactly(manual2)
end
end
end
+ end
- context 'when no builds created' do
- let(:pipeline) { build(:ci_pipeline) }
+ describe '#has_warnings?' do
+ subject { pipeline.has_warnings? }
+ context 'build which is allowed to fail fails' do
before do
- stub_ci_pipeline_yaml_file(YAML.dump(before_script: ['ls']))
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop'
end
- it 'returns false' do
- expect(pipeline.create_builds(nil)).to be_falsey
- expect(pipeline).not_to be_persisted
+ it 'returns true' do
+ is_expected.to be_truthy
end
end
- end
-
- describe "#finished_at" do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- it "returns finished_at of latest build" do
- build = FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 60
- FactoryGirl.create :ci_build, pipeline: pipeline, finished_at: Time.now - 120
+ context 'build which is allowed to fail succeeds' do
+ before do
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop'
+ end
- expect(pipeline.finished_at.to_i).to eq(build.finished_at.to_i)
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
- it "returns nil if there is no finished build" do
- FactoryGirl.create :ci_not_started_build, pipeline: pipeline
+ context 'build is retried and succeeds' do
+ before do
+ create :ci_build, :success, pipeline: pipeline, name: 'rubocop'
+ create :ci_build, :failed, pipeline: pipeline, name: 'rspec'
+ create :ci_build, :success, pipeline: pipeline, name: 'rspec'
+ end
- expect(pipeline.finished_at).to be_nil
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
end
end
- describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:pipeline) { FactoryGirl.create :ci_pipeline, project: project }
+ describe '#status' do
+ let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
- it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- expect(pipeline.coverage).to eq("35.00")
- end
+ subject { pipeline.reload.status }
- it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- FactoryGirl.create :ci_build, pipeline: pipeline
- expect(pipeline.coverage).to eq("35.00")
- end
+ context 'on queuing' do
+ before do
+ build.enqueue
+ end
- it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- expect(pipeline.coverage).to eq("35.00")
+ it { is_expected.to eq('pending') }
end
- it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, pipeline: pipeline
- expect(pipeline.coverage).to be_nil
- end
- end
+ context 'on run' do
+ before do
+ build.enqueue
+ build.run
+ end
- describe '#retryable?' do
- subject { pipeline.retryable? }
+ it { is_expected.to eq('running') }
+ end
- context 'no failed builds' do
+ context 'on drop' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'success'
+ build.drop
end
- it 'be not retryable' do
- is_expected.to be_falsey
- end
+ it { is_expected.to eq('failed') }
end
- context 'with failed builds' do
+ context 'on success' do
before do
- FactoryGirl.create :ci_build, name: "rspec", pipeline: pipeline, status: 'running'
- FactoryGirl.create :ci_build, name: "rubocop", pipeline: pipeline, status: 'failed'
+ build.success
end
- it 'be retryable' do
- is_expected.to be_truthy
- end
+ it { is_expected.to eq('success') }
end
- end
- describe '#stages' do
- let(:pipeline2) { FactoryGirl.create :ci_pipeline, project: project }
- subject { CommitStatus.where(pipeline: [pipeline, pipeline2]).stages }
+ context 'on cancel' do
+ before do
+ build.cancel
+ end
- before do
- FactoryGirl.create :ci_build, pipeline: pipeline2, stage: 'test', stage_idx: 1
- FactoryGirl.create :ci_build, pipeline: pipeline, stage: 'build', stage_idx: 0
+ it { is_expected.to eq('canceled') }
end
- it 'return all stages' do
- is_expected.to eq(%w(build test))
+ context 'on failure and build retry' do
+ before do
+ build.drop
+ Ci::Build.retry(build)
+ end
+
+ # We are changing a state: created > failed > running
+ # Instead of: created > failed > pending
+ # Since the pipeline already run, so it should not be pending anymore
+
+ it { is_expected.to eq('running') }
end
end
- describe '#update_state' do
- it 'execute update_state after touching object' do
- expect(pipeline).to receive(:update_state).and_return(true)
- pipeline.touch
- end
+ describe '#execute_hooks' do
+ let!(:build_a) { create_build('a') }
+ let!(:build_b) { create_build('b') }
- context 'dependent objects' do
- let(:commit_status) { build :commit_status, pipeline: pipeline }
+ let!(:hook) do
+ create(:project_hook, project: project, pipeline_events: enabled)
+ end
- it 'execute update_state after saving dependent object' do
- expect(pipeline).to receive(:update_state).and_return(true)
- commit_status.save
- end
+ before do
+ ProjectWebHookWorker.drain
end
- context 'update state' do
- let(:current) { Time.now.change(usec: 0) }
- let(:build) { FactoryGirl.create :ci_build, :success, pipeline: pipeline, started_at: current - 120, finished_at: current - 60 }
+ context 'with pipeline hooks enabled' do
+ let(:enabled) { true }
before do
- build
+ WebMock.stub_request(:post, hook.url)
end
- [:status, :started_at, :finished_at, :duration].each do |param|
- it "update #{param}" do
- expect(pipeline.send(param)).to eq(build.send(param))
+ context 'with multiple builds' do
+ context 'when build is queued' do
+ before do
+ build_a.enqueue
+ build_b.enqueue
+ end
+
+ it 'receive a pending event once' do
+ expect(WebMock).to have_requested_pipeline_hook('pending').once
+ end
end
- end
- end
- end
- describe '#branch?' do
- subject { pipeline.branch? }
+ context 'when build is run' do
+ before do
+ build_a.enqueue
+ build_a.run
+ build_b.enqueue
+ build_b.run
+ end
+
+ it 'receive a running event once' do
+ expect(WebMock).to have_requested_pipeline_hook('running').once
+ end
+ end
- context 'is not a tag' do
- before do
- pipeline.tag = false
- end
+ context 'when all builds succeed' do
+ before do
+ build_a.success
+ build_b.success
+ end
- it 'return true when tag is set to false' do
- is_expected.to be_truthy
+ it 'receive a success event once' do
+ expect(WebMock).to have_requested_pipeline_hook('success').once
+ end
+ end
+
+ def have_requested_pipeline_hook(status)
+ have_requested(:post, hook.url).with do |req|
+ json_body = JSON.parse(req.body)
+ json_body['object_attributes']['status'] == status &&
+ json_body['builds'].length == 2
+ end
+ end
end
end
- context 'is not a tag' do
+ context 'with pipeline hooks disabled' do
+ let(:enabled) { false }
+
before do
- pipeline.tag = true
+ build_a.enqueue
+ build_b.enqueue
end
- it 'return false when tag is set to true' do
- is_expected.to be_falsey
+ it 'did not execute pipeline_hook after touched' do
+ expect(WebMock).not_to have_requested(:post, hook.url)
end
end
+
+ def create_build(name)
+ create(:ci_build, :created, pipeline: pipeline, name: name)
+ end
end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 474b0b1621d..3ca9231f58e 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -4,12 +4,12 @@ describe Ci::Trigger, models: true do
let(:project) { FactoryGirl.create :empty_project }
describe 'before_validation' do
- it 'should set an random token if none provided' do
+ it 'sets an random token if none provided' do
trigger = FactoryGirl.create :ci_trigger_without_token, project: project
expect(trigger.token).not_to be_nil
end
- it 'should not set an random token if one provided' do
+ it 'does not set an random token if one provided' do
trigger = FactoryGirl.create :ci_trigger, project: project
expect(trigger.token).to eq('token')
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 98f60087cf5..4e7833c3162 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -9,7 +9,7 @@ describe Ci::Variable, models: true do
subject.value = secret_value
end
- describe :value do
+ describe '#value' do
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index ba02d5fe977..d3e6a6648cc 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -13,6 +13,26 @@ describe Commit, models: true do
it { is_expected.to include_module(StaticModel) }
end
+ describe '#author' do
+ it 'looks up the author in a case-insensitive way' do
+ user = create(:user, email: commit.author_email.upcase)
+ expect(commit.author).to eq(user)
+ end
+
+ it 'caches the author' do
+ user = create(:user, email: commit.author_email)
+ expect(RequestStore).to receive(:active?).twice.and_return(true)
+ expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original
+
+ expect(commit.author).to eq(user)
+ key = "commit_author:#{commit.author_email}"
+ expect(RequestStore.store[key]).to eq(user)
+
+ expect(commit.author).to eq(user)
+ RequestStore.store.clear
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
@@ -66,6 +86,27 @@ eos
end
end
+ describe '#full_title' do
+ it "returns no_commit_message when safe_message is blank" do
+ allow(commit).to receive(:safe_message).and_return('')
+ expect(commit.full_title).to eq("--no commit message")
+ end
+
+ it "returns entire message if there is no newline" do
+ message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.full_title).to eq(message)
+ end
+
+ it "returns first line of message if there is a newLine" do
+ message = commit.safe_message.split(" ").first
+
+ allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
+ expect(commit.full_title).to eq(message)
+ end
+ end
+
describe "delegation" do
subject { commit }
@@ -212,6 +253,7 @@ eos
it 'returns the URI type at the given path' do
expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
expect(commit.uri_type('files/js/application.js')).to be(:blob)
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 96397d7c8a9..fcfa3138ce5 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -24,14 +24,14 @@ describe CommitStatus, models: true do
it { is_expected.to respond_to :running? }
it { is_expected.to respond_to :pending? }
- describe :author do
+ describe '#author' do
subject { commit_status.author }
before { commit_status.author = User.new }
it { is_expected.to eq(commit_status.user) }
end
- describe :started? do
+ describe '#started?' do
subject { commit_status.started? }
context 'without started_at' do
@@ -57,7 +57,7 @@ describe CommitStatus, models: true do
end
end
- describe :active? do
+ describe '#active?' do
subject { commit_status.active? }
%w(pending running).each do |state|
@@ -77,7 +77,7 @@ describe CommitStatus, models: true do
end
end
- describe :complete? do
+ describe '#complete?' do
subject { commit_status.complete? }
%w(success failed canceled).each do |state|
@@ -97,7 +97,7 @@ describe CommitStatus, models: true do
end
end
- describe :duration do
+ describe '#duration' do
subject { commit_status.duration }
it { is_expected.to eq(120.0) }
@@ -122,7 +122,7 @@ describe CommitStatus, models: true do
end
end
- describe :latest do
+ describe '.latest' do
subject { CommitStatus.latest.order(:id) }
before do
@@ -133,12 +133,12 @@ describe CommitStatus, models: true do
@commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
end
- it 'return unique statuses' do
+ it 'returns unique statuses' do
is_expected.to eq([@commit4, @commit5])
end
end
- describe :running_or_pending do
+ describe '.running_or_pending' do
subject { CommitStatus.running_or_pending.order(:id) }
before do
@@ -149,7 +149,7 @@ describe CommitStatus, models: true do
@commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
end
- it 'return statuses that are running or pending' do
+ it 'returns statuses that are running or pending' do
is_expected.to eq([@commit1, @commit2])
end
end
@@ -160,7 +160,7 @@ describe CommitStatus, models: true do
context 'when no before_sha is set for pipeline' do
before { pipeline.before_sha = nil }
- it 'return blank sha' do
+ it 'returns blank sha' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
end
end
@@ -169,7 +169,7 @@ describe CommitStatus, models: true do
let(:value) { '1234' }
before { pipeline.before_sha = value }
- it 'return the set value' do
+ it 'returns the set value' do
is_expected.to eq(value)
end
end
@@ -177,30 +177,44 @@ describe CommitStatus, models: true do
describe '#stages' do
before do
- FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'success'
- FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'build', stage_idx: 0, status: 'failed'
- FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'deploy', stage_idx: 2, status: 'running'
- FactoryGirl.create :commit_status, pipeline: pipeline, stage: 'test', stage_idx: 1, status: 'success'
+ 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 'return ordered list of stages' do
+ 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).stages_status }
+ subject { CommitStatus.where(pipeline: pipeline).latest.stages_status }
- it 'return list of stages with statuses' do
+ 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
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
new file mode 100644
index 00000000000..49ab3c4b6e9
--- /dev/null
+++ b/spec/models/compare_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Compare, models: true do
+ include RepoHelpers
+
+ let(:project) { create(:project, :public) }
+ let(:commit) { project.commit }
+
+ let(:start_commit) { sample_image_commit }
+ let(:head_commit) { sample_commit }
+
+ let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, start_commit.id, head_commit.id) }
+
+ subject { described_class.new(raw_compare, project) }
+
+ describe '#start_commit' do
+ it 'returns raw compare base commit' do
+ expect(subject.start_commit.id).to eq(start_commit.id)
+ end
+
+ it 'returns nil if compare base commit is nil' do
+ expect(raw_compare).to receive(:base).and_return(nil)
+
+ expect(subject.start_commit).to eq(nil)
+ end
+ end
+
+ describe '#commit' do
+ it 'returns raw compare head commit' do
+ expect(subject.commit.id).to eq(head_commit.id)
+ end
+
+ it 'returns nil if compare head commit is nil' do
+ expect(raw_compare).to receive(:head).and_return(nil)
+
+ expect(subject.commit).to eq(nil)
+ end
+ end
+
+ describe '#base_commit' do
+ let(:base_commit) { Commit.new(another_sample_commit, project) }
+
+ it 'returns project merge base commit' do
+ expect(project).to receive(:merge_base_commit).with(start_commit.id, head_commit.id).and_return(base_commit)
+
+ expect(subject.base_commit).to eq(base_commit)
+ end
+
+ it 'returns nil if there is no start_commit' do
+ expect(subject).to receive(:start_commit).and_return(nil)
+
+ expect(subject.base_commit).to eq(nil)
+ end
+
+ it 'returns nil if there is no head commit' do
+ expect(subject).to receive(:head_commit).and_return(nil)
+
+ expect(subject.base_commit).to eq(nil)
+ end
+ end
+
+ describe '#diff_refs' do
+ it 'uses base_commit sha as base_sha' do
+ expect(subject).to receive(:base_commit).at_least(:once).and_call_original
+
+ expect(subject.diff_refs.base_sha).to eq(subject.base_commit.id)
+ end
+
+ it 'uses start_commit sha as start_sha' do
+ expect(subject.diff_refs.start_sha).to eq(start_commit.id)
+ end
+
+ it 'uses commit sha as head sha' do
+ expect(subject.diff_refs.head_sha).to eq(head_commit.id)
+ end
+ end
+end
diff --git a/spec/models/concerns/faster_cache_keys_spec.rb b/spec/models/concerns/faster_cache_keys_spec.rb
new file mode 100644
index 00000000000..8d3f94267fa
--- /dev/null
+++ b/spec/models/concerns/faster_cache_keys_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe FasterCacheKeys do
+ describe '#cache_key' do
+ it 'returns a String' do
+ # We're using a fixed string here so it's easier to set an expectation for
+ # the resulting cache key.
+ time = '2016-08-08 16:39:00+02'
+ issue = build(:issue, updated_at: time)
+ issue.extend(described_class)
+
+ expect(issue).to receive(:id).and_return(1)
+
+ expect(issue.cache_key).to eq("issues/1-#{time}")
+ end
+ end
+end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
new file mode 100644
index 00000000000..e118432d098
--- /dev/null
+++ b/spec/models/concerns/has_status_spec.rb
@@ -0,0 +1,129 @@
+require 'spec_helper'
+
+describe HasStatus do
+ before do
+ @object = Object.new
+ @object.extend(HasStatus::ClassMethods)
+ end
+
+ describe '.status' do
+ before do
+ allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
+ end
+
+ subject { @object.status }
+
+ shared_examples 'build status summary' do
+ context 'all successful' do
+ let(:statuses) { Array.new(2) { create(type, status: :success) } }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'at least one failed' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :failed)]
+ end
+
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'at least one running' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :running)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'at least one pending' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :pending)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'success and failed but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :failed, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one failed but allowed to fail' do
+ let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'success and canceled' do
+ let(:statuses) do
+ [create(type, status: :success), create(type, status: :canceled)]
+ end
+
+ it { is_expected.to eq 'canceled' }
+ end
+
+ context 'one failed and one canceled' do
+ let(:statuses) do
+ [create(type, status: :failed), create(type, status: :canceled)]
+ end
+
+ it { is_expected.to eq 'failed' }
+ end
+
+ context 'one failed but allowed to fail and one canceled' do
+ let(:statuses) do
+ [create(type, status: :failed, allow_failure: true),
+ create(type, status: :canceled)]
+ end
+
+ it { is_expected.to eq 'canceled' }
+ end
+
+ context 'one running one canceled' do
+ let(:statuses) do
+ [create(type, status: :running), create(type, status: :canceled)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'all canceled' do
+ let(:statuses) do
+ [create(type, status: :canceled), create(type, status: :canceled)]
+ end
+ it { is_expected.to eq 'canceled' }
+ end
+
+ context 'success and canceled but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :canceled, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'success' }
+ end
+
+ context 'one finished and second running but allowed to fail' do
+ let(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :running, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+ end
+
+ context 'ci build statuses' do
+ let(:type) { :ci_build }
+ it_behaves_like 'build status summary'
+ end
+
+ context 'generic commit statuses' do
+ let(:type) { :generic_commit_status }
+ it_behaves_like 'build status summary'
+ end
+ end
+end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 0344dae8b5d..549b0042038 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -7,7 +7,7 @@ describe Mentionable do
nil
end
- describe :references do
+ describe 'references' do
let(:project) { create(:project) }
it 'excludes JIRA references' do
@@ -68,7 +68,7 @@ describe Issue, "Mentionable" do
describe '#create_cross_references!' do
let(:project) { create(:project) }
- let(:author) { double('author') }
+ let(:author) { build(:user) }
let(:commit) { project.commit }
let(:commit2) { project.commit }
@@ -88,6 +88,10 @@ describe Issue, "Mentionable" do
let(:author) { create(:author) }
let(:issues) { create_list(:issue, 2, project: project, author: author) }
+ before do
+ project.team << [author, Gitlab::Access::DEVELOPER]
+ end
+
context 'before changes are persisted' do
it 'ignores pre-existing references' do
issue = create_issue(description: issues[0].to_reference)
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 7e9ab8940cf..b7e973798a3 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -26,53 +26,53 @@ describe Milestone, 'Milestoneish' do
end
describe '#closed_items_count' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' do
expect(milestone.closed_items_count(non_member)).to eq 2
end
- it 'should not count confidential issues for project members with guest role' do
+ it 'does not count confidential issues for project members with guest role' do
expect(milestone.closed_items_count(guest)).to eq 2
end
- it 'should count confidential issues for author' do
+ it 'counts confidential issues for author' do
expect(milestone.closed_items_count(author)).to eq 4
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.closed_items_count(assignee)).to eq 4
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.closed_items_count(member)).to eq 6
end
- it 'should count all issues for admin' do
+ it 'counts all issues for admin' do
expect(milestone.closed_items_count(admin)).to eq 6
end
end
describe '#total_items_count' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' do
expect(milestone.total_items_count(non_member)).to eq 4
end
- it 'should not count confidential issues for project members with guest role' do
+ it 'does not count confidential issues for project members with guest role' do
expect(milestone.total_items_count(guest)).to eq 4
end
- it 'should count confidential issues for author' do
+ it 'counts confidential issues for author' do
expect(milestone.total_items_count(author)).to eq 7
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.total_items_count(assignee)).to eq 7
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.total_items_count(member)).to eq 10
end
- it 'should count all issues for admin' do
+ it 'counts all issues for admin' do
expect(milestone.total_items_count(admin)).to eq 10
end
end
@@ -91,27 +91,27 @@ describe Milestone, 'Milestoneish' do
end
describe '#percent_complete' do
- it 'should not count confidential issues for non project members' do
+ it 'does not count confidential issues for non project members' do
expect(milestone.percent_complete(non_member)).to eq 50
end
- it 'should not count confidential issues for project members with guest role' do
+ it 'does not count confidential issues for project members with guest role' do
expect(milestone.percent_complete(guest)).to eq 50
end
- it 'should count confidential issues for author' do
+ it 'counts confidential issues for author' do
expect(milestone.percent_complete(author)).to eq 57
end
- it 'should count confidential issues for assignee' do
+ it 'counts confidential issues for assignee' do
expect(milestone.percent_complete(assignee)).to eq 57
end
- it 'should count confidential issues for project members' do
+ it 'counts confidential issues for project members' do
expect(milestone.percent_complete(member)).to eq 60
end
- it 'should count confidential issues for admin' do
+ it 'counts confidential issues for admin' do
expect(milestone.percent_complete(admin)).to eq 60
end
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
new file mode 100644
index 00000000000..32935bc0b09
--- /dev/null
+++ b/spec/models/concerns/spammable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Issue, 'Spammable' do
+ let(:issue) { create(:issue, description: 'Test Desc.') }
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
+ end
+
+ describe 'ClassMethods' do
+ it 'should return correct attr_spammable' do
+ expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
+ end
+ end
+
+ describe 'InstanceMethods' do
+ it 'should be invalid if spam' do
+ issue = build(:issue, spam: true)
+ expect(issue.valid?).to be_falsey
+ end
+
+ describe '#check_for_spam?' do
+ it 'returns true for public project' do
+ issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ expect(issue.check_for_spam?).to eq(true)
+ end
+
+ it 'returns false for other visibility levels' do
+ expect(issue.check_for_spam?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/statuseable_spec.rb
deleted file mode 100644
index 8e0a2a2cbde..00000000000
--- a/spec/models/concerns/statuseable_spec.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-require 'spec_helper'
-
-describe Statuseable do
- before do
- @object = Object.new
- @object.extend(Statuseable::ClassMethods)
- end
-
- describe '.status' do
- before do
- allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
- end
-
- subject { @object.status }
-
- shared_examples 'build status summary' do
- context 'all successful' do
- let(:statuses) { Array.new(2) { create(type, status: :success) } }
- it { is_expected.to eq 'success' }
- end
-
- context 'at least one failed' do
- let(:statuses) do
- [create(type, status: :success), create(type, status: :failed)]
- end
-
- it { is_expected.to eq 'failed' }
- end
-
- context 'at least one running' do
- let(:statuses) do
- [create(type, status: :success), create(type, status: :running)]
- end
-
- it { is_expected.to eq 'running' }
- end
-
- context 'at least one pending' do
- let(:statuses) do
- [create(type, status: :success), create(type, status: :pending)]
- end
-
- it { is_expected.to eq 'running' }
- end
-
- context 'success and failed but allowed to fail' do
- let(:statuses) do
- [create(type, status: :success),
- create(type, status: :failed, allow_failure: true)]
- end
-
- it { is_expected.to eq 'success' }
- end
-
- context 'one failed but allowed to fail' do
- let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
- it { is_expected.to eq 'success' }
- end
-
- context 'success and canceled' do
- let(:statuses) do
- [create(type, status: :success), create(type, status: :canceled)]
- end
-
- it { is_expected.to eq 'canceled' }
- end
-
- context 'one failed and one canceled' do
- let(:statuses) do
- [create(type, status: :failed), create(type, status: :canceled)]
- end
-
- it { is_expected.to eq 'failed' }
- end
-
- context 'one failed but allowed to fail and one canceled' do
- let(:statuses) do
- [create(type, status: :failed, allow_failure: true),
- create(type, status: :canceled)]
- end
-
- it { is_expected.to eq 'canceled' }
- end
-
- context 'one running one canceled' do
- let(:statuses) do
- [create(type, status: :running), create(type, status: :canceled)]
- end
-
- it { is_expected.to eq 'running' }
- end
-
- context 'all canceled' do
- let(:statuses) do
- [create(type, status: :canceled), create(type, status: :canceled)]
- end
- it { is_expected.to eq 'canceled' }
- end
-
- context 'success and canceled but allowed to fail' do
- let(:statuses) do
- [create(type, status: :success),
- create(type, status: :canceled, allow_failure: true)]
- end
-
- it { is_expected.to eq 'success' }
- end
-
- context 'one finished and second running but allowed to fail' do
- let(:statuses) do
- [create(type, status: :success),
- create(type, status: :running, allow_failure: true)]
- end
-
- it { is_expected.to eq 'running' }
- end
- end
-
- context 'ci build statuses' do
- let(:type) { :ci_build }
- it_behaves_like 'build status summary'
- end
-
- context 'generic commit statuses' do
- let(:type) { :generic_commit_status }
- it_behaves_like 'build status summary'
- end
- end
-end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 9e8ebc56a31..eb64f3d0c83 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -41,7 +41,7 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
describe 'ensured! token' do
subject { described_class.new.send("ensure_#{token_field}!") }
- it 'should persist new token' do
+ it 'persists new token' do
expect(subject).to eq described_class.current[token_field]
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index b273018707f..bfff639ad78 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -11,7 +11,32 @@ describe Deployment, models: true do
it { is_expected.to delegate_method(:name).to(:environment).with_prefix }
it { is_expected.to delegate_method(:commit).to(:project) }
it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) }
+ it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) }
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+
+ describe '#includes_commit?' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+ let(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit.id)
+ end
+
+ context 'when there is no project commit' do
+ it 'returns false' do
+ commit = project.commit('feature')
+
+ expect(deployment.includes_commit?(commit)).to be false
+ end
+ end
+
+ context 'when they share the same tree branch' do
+ it 'returns true' do
+ commit = project.commit
+
+ expect(deployment.includes_commit?(commit)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index af8e890ca95..6a640474cfe 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -103,7 +103,7 @@ describe DiffNote, models: true do
describe "#active?" do
context "when noteable is a commit" do
- subject { create(:diff_note_on_commit, project: project, position: position) }
+ subject { build(:diff_note_on_commit, project: project, position: position) }
it "returns true" do
expect(subject.active?).to be true
@@ -119,7 +119,7 @@ describe DiffNote, models: true do
context "when the merge request's diff refs don't match that of the diff note" do
before do
- allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
+ allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs)
end
it "returns false" do
@@ -168,7 +168,7 @@ describe DiffNote, models: true do
context "when the note is outdated" do
before do
- allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
+ allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs)
end
it "uses the DiffPositionUpdateService" do
@@ -188,4 +188,300 @@ describe DiffNote, models: true do
end
end
end
+
+ describe "#resolvable?" do
+ context "when noteable is a commit" do
+ subject { create(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when noteable is a merge request" do
+ context "when a system note" do
+ before do
+ subject.system = true
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when a regular note" do
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when already resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved status" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when not yet resolved" do
+ it "returns true" do
+ expect(subject.resolve!(current_user)).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns true" do
+ expect(subject.unresolve!).to be true
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+ end
+ end
+
+ describe "#discussion" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.discussion).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
+ let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ it "returns the discussion this note is in" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject, diff_note2])
+ end
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
+
+ describe "#original_discussion_id" do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.original_discussion_id).not_to be_nil
+ expect(note.original_discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:original_discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The original_discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.original_discussion_id).not_to be_nil
+ expect(reloaded_note.original_discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
new file mode 100644
index 00000000000..179f2e73662
--- /dev/null
+++ b/spec/models/discussion_spec.rb
@@ -0,0 +1,615 @@
+require 'spec_helper'
+
+describe Discussion, model: true do
+ subject { described_class.new([first_note, second_note, third_note]) }
+
+ let(:first_note) { create(:diff_note_on_merge_request) }
+ let(:second_note) { create(:diff_note_on_merge_request) }
+ let(:third_note) { create(:diff_note_on_merge_request) }
+
+ describe "#resolvable?" do
+ context "when a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(true)
+ end
+
+ context "when all notes are unresolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(false)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when some notes are unresolvable and some notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+
+ context "when all notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(true)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#can_resolve?" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when not signed in" do
+ let(:current_user) { nil }
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when signed in" do
+ context "when the signed in user is the noteable author" do
+ before do
+ subject.noteable.author = current_user
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user can push to the project" do
+ before do
+ subject.project.team << [current_user, :master]
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user is a random user" do
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "doesn't change resolved_at on the resolved notes" do
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved notes" do
+ expect(first_note.resolved_by).to eq(user)
+ expect(third_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved notes" do
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved state" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "doesn't change resolved_at on the resolved note" do
+ expect(first_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved note" do
+ expect(first_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved note" do
+ expect(first_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ end
+
+ it "sets resolved_at on the unresolved note" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved note" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved note as resolved" do
+ subject.resolve!(current_user)
+
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "calls resolve! on every resolvable note" do
+ expect(first_note).to receive(:resolve!).with(current_user)
+ expect(second_note).not_to receive(:resolve!)
+ expect(third_note).to receive(:resolve!).with(current_user)
+
+ subject.resolve!(current_user)
+ end
+
+ it "sets resolved_at on the unresolved notes" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved notes" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved_by).to eq(current_user)
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved notes as resolved" do
+ subject.resolve!(current_user)
+
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+
+ it "unsets resolved_at on the resolved notes" do
+ subject.unresolve!
+
+ expect(first_note.resolved_at).to be_nil
+ expect(third_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved notes" do
+ subject.unresolve!
+
+ expect(first_note.resolved_by).to be_nil
+ expect(third_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved notes as resolved" do
+ subject.unresolve!
+
+ expect(first_note.resolved?).to be false
+ expect(third_note.resolved?).to be false
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+
+ it "unsets resolved_at on the resolved note" do
+ subject.unresolve!
+
+ expect(first_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved note" do
+ subject.unresolve!
+
+ expect(first_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved note as resolved" do
+ subject.unresolve!
+
+ expect(first_note.resolved?).to be false
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "calls unresolve! on every resolvable note" do
+ expect(first_note).to receive(:unresolve!)
+ expect(second_note).not_to receive(:unresolve!)
+ expect(third_note).to receive(:unresolve!)
+
+ subject.unresolve!
+ end
+ end
+ end
+ end
+
+ describe "#collapsed?" do
+ context "when a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(true)
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.collapsed?).to be true
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+ end
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ context "when active" do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+
+ context "when outdated" do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.collapsed?).to be true
+ end
+ end
+ end
+ end
+
+ context "when not a diff discussion" do
+ before do
+ allow(subject).to receive(:diff_discussion?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.collapsed?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 7629af6a570..c881897926e 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -11,4 +11,56 @@ describe Environment, models: true do
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(:external_url).is_within(0..255) }
+
+ # 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
+
+ describe '#nullify_external_url' do
+ it 'replaces a blank url with nil' do
+ env = build(:environment, external_url: "")
+
+ expect(env.save).to be true
+ expect(env.external_url).to be_nil
+ end
+ end
+
+ describe '#includes_commit?' do
+ context 'without a last deployment' do
+ it "returns false" do
+ expect(environment.includes_commit?('HEAD')).to be false
+ end
+ end
+
+ context 'with a last deployment' do
+ let(:project) { create(:project) }
+ let(:environment) { create(:environment, project: project) }
+
+ let!(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ end
+
+ context 'in the same branch' do
+ it 'returns true' do
+ expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be true
+ end
+ end
+
+ context 'not in the same branch' do
+ before do
+ deployment.update(sha: project.commit('feature').id)
+ end
+
+ it 'returns false' do
+ expect(environment.includes_commit?(RepoHelpers.sample_commit)).to be false
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index fa1a0d4e0c7..9c81d159cdf 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -9,16 +9,16 @@ describe ForkedProjectLink, "add link on fork" do
@project_to = fork_project(project_from, user)
end
- it "project_to should know it is forked" do
+ it "project_to knows it is forked" do
expect(@project_to.forked?).to be_truthy
end
- it "project should know who it is forked from" do
+ it "project knows who it is forked from" do
expect(@project_to.forked_from_project).to eq(project_from)
end
end
-describe :forked_from_project do
+describe '#forked?' do
let(:forked_project_link) { build(:forked_project_link) }
let(:project_from) { create(:project) }
let(:project_to) { create(:project, forked_project_link: forked_project_link) }
@@ -29,15 +29,15 @@ describe :forked_from_project do
forked_project_link.save!
end
- it "project_to should know it is forked" do
+ it "project_to knows it is forked" do
expect(project_to.forked?).to be_truthy
end
- it "project_from should not be forked" do
+ it "project_from is not forked" do
expect(project_from.forked?).to be_falsey
end
- it "project_to.destroy should destroy fork_link" do
+ it "project_to.destroy destroys fork_link" do
expect(forked_project_link).to receive(:destroy)
project_to.destroy
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index c4e781dd1dc..615cfe3142b 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -4,33 +4,33 @@ describe GenericCommitStatus, models: true do
let(:pipeline) { FactoryGirl.create :ci_pipeline }
let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, pipeline: pipeline }
- describe :context do
+ describe '#context' do
subject { generic_commit_status.context }
before { generic_commit_status.context = 'my_context' }
it { is_expected.to eq(generic_commit_status.name) }
end
- describe :tags do
+ describe '#tags' do
subject { generic_commit_status.tags }
it { is_expected.to eq([:external]) }
end
- describe :set_default_values do
+ describe 'set_default_values' do
before do
generic_commit_status.context = nil
generic_commit_status.stage = nil
generic_commit_status.save
end
- describe :context do
+ describe '#context' do
subject { generic_commit_status.context }
it { is_expected.not_to be_nil }
end
- describe :stage do
+ describe '#stage' do
subject { generic_commit_status.stage }
it { is_expected.not_to be_nil }
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 197c99cd007..92e0f7f27ce 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -14,7 +14,7 @@ describe GlobalMilestone, models: true do
let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
- describe :build_collection do
+ describe '.build_collection' do
before do
milestones =
[
@@ -29,20 +29,20 @@ describe GlobalMilestone, models: true do
@global_milestones = GlobalMilestone.build_collection(milestones)
end
- it 'should have all project milestones' do
+ it 'has all project milestones' do
expect(@global_milestones.count).to eq(2)
end
- it 'should have all project milestones titles' do
+ it 'has all project milestones titles' do
expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
end
- it 'should have all project milestones' do
+ it 'has all project milestones' do
expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
end
end
- describe :initialize do
+ describe '#initialize' do
before do
milestones =
[
@@ -54,19 +54,19 @@ describe GlobalMilestone, models: true do
@global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
end
- it 'should have exactly one group milestone' do
+ it 'has exactly one group milestone' do
expect(@global_milestone.title).to eq('Milestone v1.2')
end
- it 'should have all project milestones with the same title' do
+ it 'has all project milestones with the same title' do
expect(@global_milestone.milestones.count).to eq(3)
end
end
- describe :safe_title do
+ describe '#safe_title' do
let(:milestone) { create(:milestone, title: "git / test", project: project1) }
- it 'should strip out slashes and spaces' do
+ it 'strips out slashes and spaces' do
global_milestone = GlobalMilestone.new(milestone.title, [milestone])
expect(global_milestone.safe_title).to eq('git-test')
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index a878ff1b227..ea4b59c26b1 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -97,26 +97,26 @@ describe Group, models: true do
end
end
- describe :users do
+ describe '#users' do
it { expect(group.users).to eq(group.owners) }
end
- describe :human_name do
+ describe '#human_name' do
it { expect(group.human_name).to eq(group.name) }
end
- describe :add_users do
+ describe '#add_user' do
let(:user) { create(:user) }
before { group.add_user(user, GroupMember::MASTER) }
it { expect(group.group_members.masters.map(&:user)).to include(user) }
end
- describe :add_users do
+ describe '#add_users' do
let(:user) { create(:user) }
before { group.add_users([user.id], GroupMember::GUEST) }
- it "should update the group permission" do
+ it "updates the group permission" do
expect(group.group_members.guests.map(&:user)).to include(user)
group.add_users([user.id], GroupMember::DEVELOPER)
expect(group.group_members.developers.map(&:user)).to include(user)
@@ -124,16 +124,16 @@ describe Group, models: true do
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:user) { create(:user) }
before { group.add_user(user, GroupMember::MASTER) }
- it "should be true if avatar is image" do
+ it "is true if avatar is image" do
group.update_attribute(:avatar, 'uploads/avatar.png')
expect(group.avatar_type).to be_truthy
end
- it "should be false if avatar is html page" do
+ it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
expect(group.avatar_type).to eq(["only images allowed"])
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 983848392b7..4a457997a4f 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -24,7 +24,7 @@ describe ProjectHook, models: true do
end
describe '.push_hooks' do
- it 'should return hooks for push events only' do
+ it 'returns hooks for push events only' do
hook = create(:project_hook, push_events: true)
create(:project_hook, push_events: false)
expect(ProjectHook.push_hooks).to eq([hook])
@@ -32,7 +32,7 @@ describe ProjectHook, models: true do
end
describe '.tag_push_hooks' do
- it 'should return hooks for tag push events only' do
+ it 'returns hooks for tag push events only' do
hook = create(:project_hook, tag_push_events: true)
create(:project_hook, tag_push_events: false)
expect(ProjectHook.tag_push_hooks).to eq([hook])
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 4078b9e4ff5..cbdf7eec082 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -38,7 +38,7 @@ describe SystemHook, models: true do
end
it "project_destroy hook" do
- Projects::DestroyService.new(project, user, {}).pending_delete!
+ Projects::DestroyService.new(project, user, {}).async_execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index b87d68283e6..3259f795296 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,6 +22,26 @@ 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
it 'returns a String reference to the object' do
expect(subject.to_reference).to eq "##{subject.iid}"
@@ -286,4 +306,257 @@ describe Issue, models: true do
expect(user2.assigned_open_issues_count).to eq(1)
end
end
+
+ describe '#visible_to_user?' do
+ context 'with a user' do
+ let(:user) { build(:user) }
+ let(:issue) { build(:issue) }
+
+ it 'returns true when the issue is readable' do
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(issue.visible_to_user?(user)).to eq(true)
+ end
+
+ it 'returns false when the issue is not readable' do
+ expect(issue).to receive(:readable_by?).with(user).and_return(false)
+
+ expect(issue.visible_to_user?(user)).to eq(false)
+ end
+ end
+
+ 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
+ end
+
+ describe '#readable_by?' do
+ describe 'with a regular user that is not a team member' do
+ let(:user) { create(:user) }
+
+ context 'using a public project' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, project: project, confidential: true)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ context 'using an internal user' do
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+
+ context 'using an external user' do
+ before do
+ allow(user).to receive(:external?).and_return(true)
+ end
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ context 'when the user is the project owner' do
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_readable_by(user)
+ end
+ end
+ end
+ end
+
+ context 'with a regular user that is a team member' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ context 'using a public project' do
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ before do
+ project.team << [user, Gitlab::Access::DEVELOPER]
+ end
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+ end
+
+ context 'with an admin user' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user, admin: true) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+
+ it 'returns true for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).to be_readable_by(user)
+ end
+ end
+ end
+
+ describe '#publicly_visible?' do
+ context 'using a public project' do
+ let(:project) { create(:empty_project, :public) }
+
+ it 'returns true for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+
+ context 'using an internal project' do
+ let(:project) { create(:empty_project, :internal) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+
+ context 'using a private project' do
+ let(:project) { create(:empty_project, :private) }
+
+ it 'returns false for a regular issue' do
+ issue = build(:issue, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+
+ it 'returns false for a confidential issue' do
+ issue = build(:issue, :confidential, project: project)
+
+ expect(issue).not_to be_publicly_visible
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 49cf3d8633a..fd4a2beff58 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -16,12 +16,13 @@ describe Key, models: true do
end
describe "Methods" do
+ let(:user) { create(:user) }
it { is_expected.to respond_to :projects }
it { is_expected.to respond_to :publishable_key }
describe "#publishable_keys" do
- it 'strips all personal information' do
- expect(build(:key).publishable_key).not_to match(/dummy@gitlab/)
+ it 'replaces SSH key comment with simple identifier of username + hostname' do
+ expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)")
end
end
end
@@ -72,13 +73,13 @@ describe Key, models: true do
end
context 'callbacks' do
- it 'should add new key to authorized_file' 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
end
- it 'should remove key from authorized_file' do
+ 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
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index f37f44a608e..5a5d1a5d60c 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -5,8 +5,10 @@ describe Label, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
+
it { is_expected.to have_many(:label_links).dependent(:destroy) }
it { is_expected.to have_many(:issues).through(:label_links).source(:target) }
+ it { is_expected.to have_many(:lists).dependent(:destroy) }
end
describe 'modules' do
@@ -18,7 +20,7 @@ describe Label, models: true do
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
- it 'should validate color code' do
+ it 'validates color code' do
expect(label).not_to allow_value('G-ITLAB').for(:color)
expect(label).not_to allow_value('AABBCC').for(:color)
expect(label).not_to allow_value('#AABBCCEE').for(:color)
@@ -30,7 +32,7 @@ describe Label, models: true do
expect(label).to allow_value('#abcdef').for(:color)
end
- it 'should validate title' do
+ it 'validates title' do
expect(label).not_to allow_value('G,ITLAB').for(:title)
expect(label).not_to allow_value('').for(:title)
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
index d64d89edbd3..81517a18b74 100644
--- a/spec/models/legacy_diff_note_spec.rb
+++ b/spec/models/legacy_diff_note_spec.rb
@@ -5,21 +5,21 @@ describe LegacyDiffNote, models: true do
let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
- it "should save a valid note" do
+ it "saves a valid note" do
expect(note.commit_id).to eq(commit.id)
expect(note.noteable.id).to eq(commit.id)
end
- it "should be recognized by #legacy_diff_note?" do
+ it "is recognized by #legacy_diff_note?" do
expect(note).to be_legacy_diff_note
end
end
describe '#active?' do
- it 'is always true when the note has no associated diff' do
+ it 'is always true when the note has no associated diff line' do
note = build(:legacy_diff_note_on_merge_request)
- expect(note).to receive(:diff).and_return(nil)
+ expect(note).to receive(:diff_line).and_return(nil)
expect(note).to be_active
end
@@ -27,7 +27,7 @@ describe LegacyDiffNote, models: true do
it 'is never true when the note has no noteable associated' do
note = build(:legacy_diff_note_on_merge_request)
- expect(note).to receive(:diff).and_return(double)
+ expect(note).to receive(:diff_line).and_return(double)
expect(note).to receive(:noteable).and_return(nil)
expect(note).not_to be_active
@@ -47,7 +47,7 @@ describe LegacyDiffNote, models: true do
merge = build_stubbed(:merge_request, :simple)
note = build(:legacy_diff_note_on_merge_request, noteable: merge)
- allow(note).to receive(:diff).and_return(double)
+ allow(note).to receive(:diff_line).and_return(double)
expect(note).to receive(:find_noteable_diff).and_return(nil)
expect(note).not_to be_active
@@ -58,7 +58,7 @@ describe LegacyDiffNote, models: true do
# Generate a real line_code value so we know it will match. We use a
# random line from a random diff just for funsies.
- diff = merge.diffs.to_a.sample
+ diff = merge.raw_diffs.to_a.sample
line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
@@ -73,4 +73,29 @@ describe LegacyDiffNote, models: true do
end
end
end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
new file mode 100644
index 00000000000..9e1a52011c3
--- /dev/null
+++ b/spec/models/list_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+describe List do
+ describe 'relationships' do
+ it { is_expected.to belong_to(:board) }
+ it { is_expected.to belong_to(:label) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:board) }
+ it { is_expected.to validate_presence_of(:label) }
+ it { is_expected.to validate_presence_of(:list_type) }
+ it { is_expected.to validate_presence_of(:position) }
+ it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) }
+
+ it 'validates uniqueness of label scoped to board_id' do
+ create(:list)
+
+ expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
+ end
+
+ context 'when list_type is set to backlog' do
+ subject { described_class.new(list_type: :backlog) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+
+ context 'when list_type is set to done' do
+ subject { described_class.new(list_type: :done) }
+
+ it { is_expected.not_to validate_presence_of(:label) }
+ it { is_expected.not_to validate_presence_of(:position) }
+ end
+ end
+
+ describe '#destroy' do
+ it 'can be destroyed when when list_type is set to label' do
+ subject = create(:list)
+
+ expect(subject.destroy).to be_truthy
+ end
+
+ it 'can not be destroyed when list_type is set to backlog' do
+ subject = create(:backlog_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+
+ it 'can not be destroyed when when list_type is set to done' do
+ subject = create(:done_list)
+
+ expect(subject.destroy).to be_falsey
+ end
+ end
+
+ describe '#destroyable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_destroyable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_destroyable
+ end
+ end
+
+ describe '#movable?' do
+ it 'retruns true when list_type is set to label' do
+ subject.list_type = :label
+
+ expect(subject).to be_movable
+ end
+
+ it 'retruns false when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject).not_to be_movable
+ end
+
+ it 'retruns false when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject).not_to be_movable
+ end
+ end
+
+ describe '#title' do
+ it 'returns label name when list_type is set to label' do
+ subject.list_type = :label
+ subject.label = Label.new(name: 'Development')
+
+ expect(subject.title).to eq 'Development'
+ end
+
+ it 'returns Backlog when list_type is set to backlog' do
+ subject.list_type = :backlog
+
+ expect(subject.title).to eq 'Backlog'
+ end
+
+ it 'returns Done when list_type is set to done' do
+ subject.list_type = :done
+
+ expect(subject.title).to eq 'Done'
+ end
+ end
+end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 40181a8b906..fef90d9b5cb 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -10,7 +10,7 @@ describe Member, models: true do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:source) }
- it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.all_values) }
it_behaves_like 'an object with email-formated attributes', :invite_email do
subject { build(:project_member) }
@@ -65,11 +65,21 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
- ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto1@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
- ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto2@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@@ -79,6 +89,18 @@ describe Member, models: true do
@accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request }
end
+ describe '.access_for_user_ids' do
+ it 'returns the right access levels' do
+ users = [@owner_user.id, @master_user.id]
+ expected = {
+ @owner_user.id => Gitlab::Access::OWNER,
+ @master_user.id => Gitlab::Access::MASTER
+ }
+
+ expect(described_class.access_for_user_ids(users)).to eq(expected)
+ end
+ end
+
describe '.invite' do
it { expect(described_class.invite).not_to include @master }
it { expect(described_class.invite).to include @invited_member }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 18439cac2a4..4f875fd257a 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -22,7 +22,7 @@ require 'spec_helper'
describe GroupMember, models: true do
describe 'notifications' do
describe "#after_create" do
- it "should send email to user" do
+ it "sends email to user" do
membership = build(:group_member)
allow(membership).to receive(:notification_service).
@@ -40,7 +40,7 @@ describe GroupMember, models: true do
and_return(double('NotificationService').as_null_object)
end
- it "should send email to user" do
+ it "sends email to user" do
expect(@group_member).to receive(:notification_service)
@group_member.update_attribute(:access_level, GroupMember::MASTER)
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 4c103462433..913d74645a7 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -27,6 +27,7 @@ describe ProjectMember, models: true do
describe 'validations' do
it { is_expected.to allow_value('Project').for(:source_type) }
it { is_expected.not_to allow_value('project').for(:source_type) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
end
describe 'modules' do
@@ -40,7 +41,7 @@ describe ProjectMember, models: true do
end
describe "#destroy" do
- let(:owner) { create(:project_member, access_level: ProjectMember::OWNER) }
+ let(:owner) { create(:project_member, access_level: ProjectMember::MASTER) }
let(:project) { owner.project }
let(:master) { create(:project_member, project: project) }
@@ -52,7 +53,7 @@ describe ProjectMember, models: true do
master_todos
end
- it "destroy itself and delete associated todos" do
+ it "destroys itself and delete associated todos" do
expect(owner.user.todos.size).to eq(2)
expect(master.user.todos.size).to eq(3)
expect(Todo.count).to eq(5)
@@ -101,7 +102,7 @@ describe ProjectMember, models: true do
end
end
- describe :add_users_into_projects do
+ describe '.add_users_to_projects' do
before do
@project_1 = create :project
@project_2 = create :project
@@ -109,7 +110,7 @@ describe ProjectMember, models: true do
@user_1 = create :user
@user_2 = create :user
- ProjectMember.add_users_into_projects(
+ ProjectMember.add_users_to_projects(
[@project_1.id, @project_2.id],
[@user_1.id, @user_2.id],
ProjectMember::MASTER
@@ -123,7 +124,7 @@ describe ProjectMember, models: true do
it { expect(@project_2.users).to include(@user_2) }
end
- describe :truncate_teams do
+ describe '.truncate_teams' do
before do
@project_1 = create :project
@project_2 = create :project
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
new file mode 100644
index 00000000000..e5b185dc3f6
--- /dev/null
+++ b/spec/models/merge_request_diff_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe MergeRequestDiff, models: true do
+ describe 'create new record' do
+ subject { create(:merge_request).merge_request_diff }
+
+ it { expect(subject).to be_valid }
+ it { expect(subject).to be_persisted }
+ it { expect(subject.commits.count).to eq(5) }
+ it { expect(subject.diffs.count).to eq(8) }
+ it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+ end
+
+ describe '#latest' do
+ let!(:mr) { create(:merge_request, :with_diffs) }
+ let!(:first_diff) { mr.merge_request_diff }
+ let!(:last_diff) { mr.create_merge_request_diff }
+
+ it { expect(last_diff.latest?).to be_truthy }
+ it { expect(first_diff.latest?).to be_falsey }
+ end
+
+ describe '#diffs' do
+ let(:mr) { create(:merge_request, :with_diffs) }
+ let(:mr_diff) { mr.merge_request_diff }
+
+ context 'when the :ignore_whitespace_change option is set' do
+ it 'creates a new compare object instead of loading from the DB' do
+ expect(mr_diff).not_to receive(:load_diffs)
+ expect(Gitlab::Git::Compare).to receive(:new).and_call_original
+
+ mr_diff.raw_diffs(ignore_whitespace_change: true)
+ end
+ end
+
+ context 'when the raw diffs are empty' do
+ before { mr_diff.update_attributes(st_diffs: '') }
+
+ it 'returns an empty DiffCollection' do
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).to be_empty
+ end
+ end
+
+ context 'when the raw diffs exist' do
+ it 'returns the diffs' do
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).not_to be_empty
+ end
+
+ context 'when the :paths option is set' do
+ let(:diffs) { mr_diff.raw_diffs(paths: ['files/ruby/popen.rb', 'files/ruby/popen.rb']) }
+
+ it 'only returns diffs that match the (old path, new path) given' do
+ expect(diffs.map(&:new_path)).to contain_exactly('files/ruby/popen.rb')
+ end
+
+ it 'uses the diffs from the DB' do
+ expect(mr_diff).to receive(:load_diffs)
+
+ diffs
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a4b6ff8f8ad..d67f71bbb9c 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
+ it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
describe 'modules' do
@@ -65,11 +65,11 @@ describe MergeRequest, models: true do
end
describe '#target_branch_sha' do
- context 'when the target branch does not exist anymore' do
- let(:project) { create(:project) }
+ let(:project) { create(:project) }
- subject { create(:merge_request, source_project: project, target_project: project) }
+ subject { create(:merge_request, source_project: project, target_project: project) }
+ context 'when the target branch does not exist' do
before do
project.repository.raw_repository.delete_branch(subject.target_branch)
end
@@ -78,6 +78,12 @@ describe MergeRequest, models: true do
expect(subject.target_branch_sha).to be_nil
end
end
+
+ it 'returns memoized value' do
+ subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'
+
+ expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
+ end
end
describe '#source_branch_sha' do
@@ -103,6 +109,12 @@ describe MergeRequest, models: true do
expect(subject.source_branch_sha).to be_nil
end
end
+
+ it 'returns memoized value' do
+ subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
+
+ expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
+ end
end
describe '#to_reference' do
@@ -116,6 +128,56 @@ describe MergeRequest, models: true do
end
end
+ describe '#raw_diffs' do
+ let(:merge_request) { build(:merge_request) }
+ let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
+
+ context 'when there are MR diffs' do
+ it 'delegates to the MR diffs' do
+ merge_request.merge_request_diff = MergeRequestDiff.new
+
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
+
+ merge_request.raw_diffs(options)
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ it 'delegates to the compare object' do
+ merge_request.compare = double(:compare)
+
+ expect(merge_request.compare).to receive(:raw_diffs).with(options)
+
+ merge_request.raw_diffs(options)
+ end
+ end
+ end
+
+ describe '#diffs' do
+ let(:merge_request) { build(:merge_request) }
+ let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }
+
+ context 'when there are MR diffs' do
+ it 'delegates to the MR diffs' do
+ merge_request.save
+
+ expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
+
+ merge_request.diffs(options)
+ end
+ end
+
+ context 'when there are no MR diffs' do
+ it 'delegates to the compare object' do
+ merge_request.compare = double(:compare)
+
+ expect(merge_request.compare).to receive(:diffs).with(options)
+
+ merge_request.diffs(options)
+ end
+ end
+ end
+
describe "#mr_and_commit_notes" do
let!(:merge_request) { create(:merge_request) }
@@ -126,12 +188,12 @@ describe MergeRequest, models: true do
create(:note, noteable: merge_request, project: merge_request.project)
end
- it "should include notes for commits" 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)
end
- it "should include notes for commits from target project as well" do
+ it "includes notes for commits from target project as well" do
create(:note_on_commit, commit_id: merge_request.commits.first.id,
project: merge_request.target_project)
@@ -242,7 +304,7 @@ describe MergeRequest, models: true do
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
- it "cant remove a root ref" do
+ it "can't remove a root ref" do
subject.source_branch = "master"
subject.target_branch = "feature"
@@ -254,7 +316,7 @@ describe MergeRequest, models: true do
end
it "can be removed if the last commit is the head of the source branch" do
- allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit)
+ allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
@@ -394,6 +456,20 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
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])
+ end
+
+ it 'returns sha of commits' do
+ expect(subject.commits_sha).to contain_exactly('sha1', 'sha2', 'sha3')
+ end
+ end
+
describe '#pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
@@ -418,6 +494,19 @@ describe MergeRequest, models: true do
end
end
+ describe '#all_pipelines' do
+ let!(:pipelines) do
+ subject.merge_request_diff.commits.map do |commit|
+ create(:ci_empty_pipeline, project: subject.source_project, sha: commit.id, ref: subject.source_branch)
+ end
+ end
+
+ it 'returns a pipelines from source projects with proper ordering' do
+ expect(subject.all_pipelines).not_to be_empty
+ expect(subject.all_pipelines).to eq(pipelines.reverse)
+ end
+ end
+
describe '#participants' do
let(:project) { create(:project, :public) }
@@ -612,13 +701,37 @@ describe MergeRequest, models: true do
end
end
+ describe "#environments" do
+ let(:project) { create(:project) }
+ let!(:environment) { create(:environment, project: project) }
+ let!(:environment1) { create(:environment, project: project) }
+ let!(:environment2) { create(:environment, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'selects deployed environments' do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ create(:deployment, environment: environment1, sha: project.commit('feature').id)
+
+ expect(merge_request.environments).to eq [environment]
+ end
+ end
+
describe "#reload_diff" do
let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
let(:commit) { subject.project.commit(sample_commit.id) }
- it "reloads the diff content" do
- expect(subject.merge_request_diff).to receive(:reload_content)
+ it "does not change existing merge request diff" do
+ expect(subject.merge_request_diff).not_to receive(:save_git_content)
+ subject.reload_diff
+ end
+
+ it "creates new merge request diff" do
+ expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
+ end
+
+ it "executs diff cache service" do
+ expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
subject.reload_diff
end
@@ -626,13 +739,15 @@ describe MergeRequest, models: true do
it "updates diff note positions" do
old_diff_refs = subject.diff_refs
- merge_request_diff = subject.merge_request_diff
-
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
- allow(merge_request_diff).to receive(:reload_content) do
- merge_request_diff.base_commit_sha = commit.parent_id
- merge_request_diff.start_commit_sha = commit.parent_id
- merge_request_diff.head_commit_sha = commit.sha
+ allow(subject).to receive(:create_merge_request_diff) do
+ subject.merge_request_diffs.create(
+ base_commit_sha: commit.parent_id,
+ start_commit_sha: commit.parent_id,
+ head_commit_sha: commit.sha
+ )
+
+ subject.merge_request_diff(true)
end
expect(Notes::DiffPositionUpdateService).to receive(:new).with(
@@ -642,11 +757,209 @@ describe MergeRequest, models: true do
new_diff_refs: commit.diff_refs,
paths: note.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff
end
end
+
+ describe '#branch_merge_base_commit' do
+ context 'source and target branch exist' do
+ it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
+ end
+
+ context 'when the target branch does not exist' do
+ before do
+ subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ end
+
+ it 'returns nil' do
+ expect(subject.branch_merge_base_commit).to be_nil
+ end
+ end
+ end
+
+ describe "#diff_sha_refs" do
+ context "with diffs" do
+ subject { create(:merge_request, :with_diffs) }
+
+ it "does not touch the repository" do
+ subject # Instantiate the object
+
+ expect_any_instance_of(Repository).not_to receive(:commit)
+
+ subject.diff_sha_refs
+ end
+
+ it "returns expected diff_refs" do
+ expected_diff_refs = Gitlab::Diff::DiffRefs.new(
+ base_sha: subject.merge_request_diff.base_commit_sha,
+ start_sha: subject.merge_request_diff.start_commit_sha,
+ head_sha: subject.merge_request_diff.head_commit_sha
+ )
+
+ expect(subject.diff_sha_refs).to eq(expected_diff_refs)
+ end
+ end
+ end
+
+ context "discussion status" do
+ let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+ let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+ let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
+
+ before do
+ allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
+ end
+
+ describe "#discussions_resolvable?" do
+ context "when all discussions are unresolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(false)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolvable?).to be false
+ end
+ end
+
+ context "when some discussions are unresolvable and some discussions are resolvable" do
+ before do
+ 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
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+
+ context "when all discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(true)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+ end
+
+ describe "#discussions_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_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 true" do
+ expect(subject.discussions_resolved?).to be true
+ 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 false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe '#conflicts_can_be_resolved_in_ui?' do
+ def create_merge_request(source_branch)
+ create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
+ mr.mark_as_unmergeable
+ end
+ end
+
+ it 'returns a falsey value when the MR can be merged without conflicts' do
+ merge_request = create_merge_request('master')
+ merge_request.mark_as_mergeable
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+ merge_request = create_merge_request('master')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR has a missing ref after a force push' do
+ merge_request = create_merge_request('conflict-resolvable')
+ allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR does not support new diff notes' do
+ merge_request = create_merge_request('conflict-resolvable')
+ merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a large file' do
+ merge_request = create_merge_request('conflict-too-large')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a binary file' do
+ merge_request = create_merge_request('conflict-binary-file')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do
+ merge_request = create_merge_request('conflict-contains-conflict-markers')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
+ merge_request = create_merge_request('conflict-missing-side')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a truthy value when the conflicts are resolvable in the UI' do
+ merge_request = create_merge_request('conflict-resolvable')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 1e18c788b50..d64d6cde2b5 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -28,12 +28,12 @@ describe Milestone, models: true do
end
describe "unique milestone title per project" do
- it "shouldn't accept the same title in a project twice" do
+ it "does not accept the same title in a project twice" do
new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
expect(new_milestone).not_to be_valid
end
- it "should accept the same title in another project" do
+ it "accepts the same title in another project" do
project = build(:project)
new_milestone = Milestone.new(project: project, title: milestone.title)
@@ -42,35 +42,35 @@ describe Milestone, models: true do
end
describe "#percent_complete" do
- it "should not count open issues" do
+ it "does not count open issues" do
milestone.issues << issue
expect(milestone.percent_complete(user)).to eq(0)
end
- it "should count closed issues" do
+ it "counts closed issues" do
issue.close
milestone.issues << issue
expect(milestone.percent_complete(user)).to eq(100)
end
- it "should recover from dividing by zero" do
+ it "recovers from dividing by zero" do
expect(milestone.percent_complete(user)).to eq(0)
end
end
describe "#expires_at" do
- it "should be nil when due_date is unset" 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 "should not be nil when due_date is set" do
+ 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
+ describe '#expired?' do
context "expired" do
before do
allow(milestone).to receive(:due_date).and_return(Date.today.prev_year)
@@ -88,7 +88,7 @@ describe Milestone, models: true do
end
end
- describe :percent_complete do
+ describe '#percent_complete' do
before do
allow(milestone).to receive_messages(
closed_items_count: 3,
@@ -111,22 +111,22 @@ describe Milestone, models: true do
it { expect(milestone.is_empty?(user)).to be_falsey }
end
- describe :can_be_closed? do
+ describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe :total_items_count do
+ describe '#total_items_count' do
before do
create :closed_issue, milestone: milestone
create :merge_request, milestone: milestone
end
- it 'Should return total count of issues and merge requests assigned to milestone' do
+ it 'returns total count of issues and merge requests assigned to milestone' do
expect(milestone.total_items_count(user)).to eq 2
end
end
- describe :can_be_closed? do
+ describe '#can_be_closed?' do
before do
milestone = create :milestone
create :closed_issue, milestone: milestone
@@ -134,11 +134,11 @@ describe Milestone, models: true do
create :issue
end
- it 'should be true if milestone active and all nested issues closed' do
+ it 'returns true if milestone active and all nested issues closed' do
expect(milestone.can_be_closed?).to be_truthy
end
- it 'should be false if milestone active and not all nested issues closed' do
+ it 'returns false if milestone active and not all nested issues closed' do
issue.milestone = milestone
issue.save
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 5f68cd2b066..544920d1824 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -18,11 +18,11 @@ describe Namespace, models: true do
it { is_expected.to respond_to(:to_param) }
end
- describe :to_param do
+ describe '#to_param' do
it { expect(namespace.to_param).to eq(namespace.path) }
end
- describe :human_name do
+ describe '#human_name' do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
@@ -54,18 +54,18 @@ describe Namespace, models: true do
end
end
- describe :move_dir do
+ describe '#move_dir' do
before do
@namespace = create :namespace
@project = create :project, namespace: @namespace
allow(@namespace).to receive(:path_changed?).and_return(true)
end
- it "should raise error when directory exists" do
+ it "raises error when directory exists" do
expect { @namespace.move_dir }.to raise_error("namespace directory cannot be moved")
end
- it "should move dir if path changed" do
+ it "moves dir if path changed" do
new_path = @namespace.path + "_new"
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return(new_path)
@@ -93,12 +93,12 @@ describe Namespace, models: true do
before { namespace.destroy }
- it "should remove its dirs when deleted" do
+ it "removes its dirs when deleted" do
expect(File.exist?(path)).to be(false)
end
end
- describe :find_by_path_or_name do
+ describe '.find_by_path_or_name' do
before do
@namespace = create(:namespace, name: 'WoW', path: 'woW')
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
new file mode 100644
index 00000000000..b76513d2a3c
--- /dev/null
+++ b/spec/models/network/graph_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Network::Graph, models: true do
+ let(:project) { create(:project) }
+ let!(:note_on_commit) { create(:note_on_commit, project: project) }
+
+ it '#initialize' do
+ graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+
+ expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6549791f675..9e8ae07e0b2 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Note, models: true do
+ include RepoHelpers
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable).touch(true) }
@@ -56,18 +58,18 @@ describe Note, models: true do
let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
- it "should be accessible through #noteable" do
+ it "is accessible through #noteable" do
expect(note.commit_id).to eq(commit.id)
expect(note.noteable).to be_a(Commit)
expect(note.noteable).to eq(commit)
end
- it "should save a valid note" do
+ it "saves a valid note" do
expect(note.commit_id).to eq(commit.id)
note.noteable == commit
end
- it "should be recognized by #for_commit?" do
+ it "is recognized by #for_commit?" do
expect(note).to be_for_commit
end
@@ -135,22 +137,30 @@ describe Note, models: true do
let!(:note2) { create(:note_on_issue) }
it "reads the rendered note body from the cache" do
- expect(Banzai::Renderer).to receive(:render).
- with(note1.note,
- pipeline: :note,
- cache_key: [note1, "note"],
- project: note1.project,
- author: note1.author)
-
- expect(Banzai::Renderer).to receive(:render).
- with(note2.note,
- pipeline: :note,
- cache_key: [note2, "note"],
- project: note2.project,
- author: note2.author)
-
- note1.all_references
- note2.all_references
+ expect(Banzai::Renderer).to receive(:cache_collection_render).
+ with([{
+ text: note1.note,
+ context: {
+ pipeline: :note,
+ cache_key: [note1, "note"],
+ project: note1.project,
+ author: note1.author
+ }
+ }]).and_call_original
+
+ expect(Banzai::Renderer).to receive(:cache_collection_render).
+ with([{
+ text: note2.note,
+ context: {
+ pipeline: :note,
+ cache_key: [note2, "note"],
+ project: note2.project,
+ author: note2.author
+ }
+ }]).and_call_original
+
+ note1.all_references.users
+ note2.all_references.users
end
end
@@ -215,7 +225,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
@@ -226,6 +236,20 @@ describe Note, models: true do
it "returns false" do
expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy
end
+
+ it "returns false if user visible reference count set" do
+ note.user_visible_reference_count = 1
+
+ expect(note).not_to receive(:reference_mentionables)
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_falsy
+ end
+
+ it "returns true if ref count is 0" do
+ note.user_visible_reference_count = 0
+
+ expect(note).not_to receive(:reference_mentionables)
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
+ end
end
describe 'clear_blank_line_code!' do
@@ -245,4 +269,81 @@ describe Note, models: true do
expect(note.participants).to include(note.author)
end
end
+
+ describe ".grouped_diff_discussions" do
+ let!(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
+ let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
+ )
+ end
+
+ subject { merge_request.notes.grouped_diff_discussions }
+
+ it "includes active discussions" do
+ discussions = subject.values
+
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
+
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
+
+ it "doesn't include outdated discussions" do
+ expect(subject.values.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[active_diff_note1.line_code].id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+ end
end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
index e12258c0874..36379074ea0 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Project, models: true do
- describe :authorization do
+ describe 'authorization' do
before do
@p1 = create(:project)
@@ -21,7 +21,7 @@ describe Project, models: true do
let(:owner_actions) { Ability.project_owner_rules }
describe "Non member rules" do
- it "should deny for non-project users any actions" do
+ it "denies for non-project users any actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u1, action, @p1)).to be_falsey
end
@@ -33,7 +33,7 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::GUEST)
end
- it "should allow for project user any guest actions" do
+ it "allows for project user any guest actions" do
guest_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
@@ -45,7 +45,7 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u2, access_level: ProjectMember::REPORTER)
end
- it "should allow for project user any report actions" do
+ it "allows for project user any report actions" do
report_actions.each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_truthy
end
@@ -58,13 +58,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::DEVELOPER)
end
- it "should deny for developer master-specific actions" do
+ it "denies for developer master-specific actions" do
[dev_actions - report_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project user any dev actions" do
+ it "allows for project user any dev actions" do
dev_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
@@ -77,13 +77,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
- it "should deny for developer master-specific actions" do
+ it "denies for developer master-specific actions" do
[master_actions - dev_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project user any master actions" do
+ it "allows for project user any master actions" do
master_actions.each do |action|
expect(@abilities.allowed?(@u3, action, @p1)).to be_truthy
end
@@ -96,13 +96,13 @@ describe Project, models: true do
@p1.project_members.create(project: @p1, user: @u3, access_level: ProjectMember::MASTER)
end
- it "should deny for masters admin-specific actions" do
+ it "denies for masters admin-specific actions" do
[owner_actions - master_actions].each do |action|
expect(@abilities.allowed?(@u2, action, @p1)).to be_falsey
end
end
- it "should allow for project owner any admin actions" do
+ it "allows for project owner any admin actions" do
owner_actions.each do |action|
expect(@abilities.allowed?(@u4, action, @p1)).to be_truthy
end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index f3d15f3c1ea..dc702cfc42c 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -65,7 +65,7 @@ describe AsanaService, models: true do
)
end
- it 'should call Asana service to create a story' do
+ it 'calls Asana service to create a story' do
data = create_data_for_commits('Message from commit. related to #123456')
expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
@@ -76,7 +76,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should call Asana service to create a story and close a task' do
+ it 'calls Asana service to create a story and close a task' do
data = create_data_for_commits('fix #456789')
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment)
@@ -86,7 +86,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should be able to close via url' do
+ it 'is able to close via url' do
data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment)
@@ -96,7 +96,7 @@ describe AsanaService, models: true do
@asana.execute(data)
end
- it 'should allow multiple matches per line' do
+ it 'allows multiple matches per line' do
message = <<-EOF
minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 17e9361dd5c..d672d80156c 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -39,12 +39,12 @@ describe AssemblaService, models: true do
token: 'verySecret',
subdomain: 'project_name'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
WebMock.stub_request(:post, @api_url)
end
- it "should call Assembla API" do
+ it "calls Assembla API" do
@assembla_service.execute(@sample_data)
expect(WebMock).to have_requested(:post, @api_url).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 60364df2015..0866e1532dd 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -57,7 +57,7 @@ describe BuildkiteService, models: true do
)
end
- describe :webhook_url do
+ describe '#webhook_url' do
it 'returns the webhook url' do
expect(@service.webhook_url).to eq(
'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token'
@@ -65,7 +65,7 @@ describe BuildkiteService, models: true do
end
end
- describe :commit_status_path do
+ describe '#commit_status_path' do
it 'returns the correct status page' do
expect(@service.commit_status_path('2ab7834c')).to eq(
'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c'
@@ -73,7 +73,7 @@ describe BuildkiteService, models: true do
end
end
- describe :build_page do
+ describe '#build_page' do
it 'returns the correct build page' do
expect(@service.build_page('2ab7834c', nil)).to eq(
'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
index 236df8f047d..0194f9e2563 100644
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe BuildsEmailService do
- let(:data) { Gitlab::BuildDataBuilder.build(create(:ci_build)) }
+ let(:data) do
+ Gitlab::DataBuilder::Build.build(create(:ci_build))
+ end
describe 'Validations' do
context 'when service is active' do
@@ -23,6 +25,44 @@ describe BuildsEmailService do
end
end
+ describe '#test_data' do
+ let(:build) { create(:ci_build) }
+ let(:project) { build.project }
+ let(:user) { create(:user) }
+
+ before { project.team << [user, :developer] }
+
+ it 'builds test data' do
+ data = subject.test_data(project)
+
+ expect(data[:object_kind]).to eq("build")
+ end
+ end
+
+ describe '#test' do
+ it 'sends email' do
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
+ subject.recipients = 'test@gitlab.com'
+
+ expect(BuildEmailWorker).to receive(:perform_async)
+
+ subject.test(data)
+ end
+
+ context 'notify only failed builds is true' do
+ it 'sends email' do
+ data = Gitlab::DataBuilder::Build.build(create(:ci_build))
+ data[:build_status] = "success"
+ subject.recipients = 'test@gitlab.com'
+
+ expect(subject).not_to receive(:notify_only_broken_builds)
+ expect(BuildEmailWorker).to receive(:perform_async)
+
+ subject.test(data)
+ end
+ end
+ end
+
describe '#execute' do
it 'sends email' do
subject.recipients = 'test@gitlab.com'
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 3e6da42803b..c76ae21421b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -39,4 +39,62 @@ describe CampfireService, models: true do
it { is_expected.not_to validate_presence_of(:token) }
end
end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ @campfire_service = CampfireService.new
+ allow(@campfire_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ subdomain: 'project-name',
+ room: 'test-room'
+ )
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+ @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
+ @headers = { 'Content-Type' => 'application/json; charset=utf-8' }
+ end
+
+ it "calls Campfire API to get a list of rooms and speak in a room" do
+ # make sure a valid list of rooms is returned
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
+ WebMock.stub_request(:get, @rooms_url).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+ # stub the speak request with the room id found in the previous request's response
+ speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json'
+ WebMock.stub_request(:post, speak_url)
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, @rooms_url).once
+ expect(WebMock).to have_requested(:post, speak_url).with(
+ body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
+ ).once
+ end
+
+ it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
+ # return a list of rooms that do not contain a room named 'test-room'
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
+ WebMock.stub_request(:get, @rooms_url).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+ # we want to make sure no request is sent to the /speak endpoint, here is a basic
+ # regexp that matches this endpoint
+ speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, @rooms_url).once
+ expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+ end
+ end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 3a8e67438fc..8ef892259f2 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -84,7 +84,9 @@ describe DroneCiService, models: true do
include_context :drone_ci_service
let(:user) { create(:user, username: 'username') }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
it do
service_hook = double
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 5fe5ea7d2df..d7c5ea95d71 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -56,7 +56,7 @@ describe ExternalWikiService, models: true do
@service.destroy!
end
- it 'should replace the wiki url' do
+ it 'replaces the wiki url' do
wiki_path = get_project_wiki_path(project)
expect(wiki_path).to match('https://gitlab.com')
end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index b7e627e6518..d2557019756 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -52,12 +52,12 @@ describe FlowdockService, models: true do
service_hook: true,
token: 'verySecret'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://api.flowdock.com/v1/messages'
WebMock.stub_request(:post, @api_url)
end
- it "should call FlowDock API" do
+ it "calls FlowDock API" do
@flowdock_service.execute(@sample_data)
@sample_data[:commits].each do |commit|
# One request to Flowdock per new commit
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index a08f1ac229f..3d0b6c9816b 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -55,9 +55,9 @@ describe GemnasiumService, models: true do
token: 'verySecret',
api_key: 'GemnasiumUserApiKey'
)
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
end
- it "should call Gemnasium service" do
+ it "calls Gemnasium service" do
expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
@gemnasium_service.execute(@sample_data)
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 7a1f106d6e3..8ef79a17d50 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -54,7 +54,7 @@ describe GitlabIssueTrackerService, models: true do
@service.destroy!
end
- it 'should give the correct path' do
+ 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")
@@ -71,7 +71,7 @@ describe GitlabIssueTrackerService, models: true do
@service.destroy!
end
- it 'should give the correct path' do
+ it 'gives the correct path' do
expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 5f618322aab..34eafbe555d 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -48,7 +48,9 @@ describe HipchatService, models: true do
let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
before(:each) do
allow(hipchat).to receive_messages(
@@ -61,7 +63,7 @@ describe HipchatService, models: true do
WebMock.stub_request(:post, api_url)
end
- it 'should test and return errors' do
+ it 'tests and return errors' do
allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
result = hipchat.test(push_sample_data)
@@ -69,7 +71,7 @@ describe HipchatService, models: true do
expect(result[:result].to_s).to eq('no such room')
end
- it 'should use v1 if version is provided' do
+ it 'uses v1 if version is provided' do
allow(hipchat).to receive(:api_version).and_return('v1')
expect(HipChat::Client).to receive(:new).with(
token,
@@ -79,7 +81,7 @@ describe HipchatService, models: true do
hipchat.execute(push_sample_data)
end
- it 'should use v2 as the version when nothing is provided' do
+ it 'uses v2 as the version when nothing is provided' do
allow(hipchat).to receive(:api_version).and_return('')
expect(HipChat::Client).to receive(:new).with(
token,
@@ -90,13 +92,13 @@ describe HipchatService, models: true do
end
context 'push events' do
- it "should call Hipchat API for push events" do
+ it "calls Hipchat API for push events" do
hipchat.execute(push_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a push message" do
+ it "creates a push message" do
message = hipchat.send(:create_push_message, push_sample_data)
push_sample_data[:object_attributes]
@@ -108,15 +110,23 @@ describe HipchatService, models: true do
end
context 'tag_push events' do
- let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build(
+ project,
+ user,
+ Gitlab::Git::BLANK_SHA,
+ '1' * 40,
+ 'refs/tags/test',
+ [])
+ end
- it "should call Hipchat API for tag push events" do
+ it "calls Hipchat API for tag push events" do
hipchat.execute(push_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a tag push message" do
+ it "creates a tag push message" do
message = hipchat.send(:create_push_message, push_sample_data)
push_sample_data[:object_attributes]
@@ -131,13 +141,13 @@ describe HipchatService, models: true do
let(:issue_service) { Issues::CreateService.new(project, user) }
let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
- it "should call Hipchat API for issue events" do
+ it "calls Hipchat API for issue events" do
hipchat.execute(issues_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create an issue message" do
+ it "creates an issue message" do
message = hipchat.send(:create_issue_message, issues_sample_data)
obj_attr = issues_sample_data[:object_attributes]
@@ -154,13 +164,13 @@ describe HipchatService, models: true do
let(:merge_service) { MergeRequests::CreateService.new(project, user) }
let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
- it "should call Hipchat API for merge requests events" do
+ it "calls Hipchat API for merge requests events" do
hipchat.execute(merge_sample_data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a merge request message" do
+ it "creates a merge request message" do
message = hipchat.send(:create_merge_request_message,
merge_sample_data)
@@ -184,8 +194,8 @@ describe HipchatService, models: true do
note: 'a comment on a commit')
end
- it "should call Hipchat API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ it "calls Hipchat API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -216,8 +226,8 @@ describe HipchatService, models: true do
note: "merge request note")
end
- it "should call Hipchat API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ it "calls Hipchat API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -243,8 +253,8 @@ describe HipchatService, models: true do
note: "issue note")
end
- it "should call Hipchat API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ it "calls Hipchat API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
hipchat.execute(data)
message = hipchat.send(:create_message, data)
@@ -269,8 +279,8 @@ describe HipchatService, models: true do
note: "snippet note")
end
- it "should call Hipchat API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ it "calls Hipchat API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
@@ -291,19 +301,20 @@ describe HipchatService, models: true do
end
context 'build events' do
- let(:build) { create(:ci_build) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
context 'for failed' do
before { build.drop }
- it "should call Hipchat API" do
+ it "calls Hipchat API" do
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should create a build message" do
+ it "creates a build message" do
message = hipchat.send(:create_build_message, data)
project_url = project.web_url
@@ -325,13 +336,13 @@ describe HipchatService, models: true do
build.success
end
- it "should call Hipchat API" do
+ it "calls Hipchat API" do
hipchat.notify_only_broken_builds = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
- it "should notify only broken" do
+ it "notifies only broken" do
hipchat.notify_only_broken_builds = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
@@ -340,18 +351,36 @@ describe HipchatService, models: true do
end
context "#message_options" do
- it "should be set to the defaults" do
- expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' })
+ it "is set to the defaults" do
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' })
end
- it "should set notfiy to true" do
+ it "sets notify to true" do
allow(hipchat).to receive(:notify).and_return('1')
- expect(hipchat.send(:message_options)).to eq({ notify: true, color: 'yellow' })
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' })
end
- it "should set the color" do
+ it "sets the color" do
allow(hipchat).to receive(:color).and_return('red')
- expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'red' })
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' })
+ end
+
+ context 'with a successful build' do
+ it 'uses the green color' do
+ build_data = { object_kind: 'build', commit: { status: 'success' } }
+
+ expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ end
+ end
+
+ context 'with a failed build' do
+ it 'uses the red color' do
+ build_data = { object_kind: 'build', commit: { status: 'failed' } }
+
+ expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ end
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 4ee022a5171..ffb17fd3259 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -46,32 +46,35 @@ describe IrkerService, models: true do
let(:irker) { IrkerService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:recipients) { '#commits irc://test.net/#test ftp://bad' }
let(:colorize_messages) { '1' }
before do
+ @irker_server = TCPServer.new 'localhost', 0
+
allow(irker).to receive_messages(
active: true,
project: project,
project_id: project.id,
service_hook: true,
- server_host: 'localhost',
- server_port: 6659,
+ server_host: @irker_server.addr[2],
+ server_port: @irker_server.addr[1],
default_irc_uri: 'irc://chat.freenode.net/',
recipients: recipients,
colorize_messages: colorize_messages)
irker.valid?
- @irker_server = TCPServer.new 'localhost', 6659
end
after do
@irker_server.close
end
- it 'should send valid JSON messages to an Irker listener' do
+ it 'sends valid JSON messages to an Irker listener' do
irker.execute(sample_data)
conn = @irker_server.accept
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 5a97cf370da..9037ca5cc20 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -66,7 +66,7 @@ describe JiraService, models: true do
password: 'gitlab_jira_password'
)
@jira_service.save # will build API URL, as api_url was not specified above
- @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
# https://github.com/bblimke/webmock#request-with-basic-authentication
@api_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'
@@ -75,7 +75,7 @@ describe JiraService, models: true do
WebMock.stub_request(:post, @comment_url)
end
- it "should call JIRA API" do
+ it "calls JIRA API" do
@jira_service.execute(merge_request,
ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
@@ -128,7 +128,7 @@ describe JiraService, models: true do
expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
end
- it "should reset password if url changed, even if setter called multiple times" do
+ it "resets password if url changed, even if setter called multiple times" do
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.api_url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
@@ -181,7 +181,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it 'should be initialized' do
+ it 'is initialized' do
expect(@service.title).to eq('JIRA')
expect(@service.description).to eq("Jira issue tracker")
end
@@ -197,7 +197,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it "should be correct" do
+ it "is correct" do
expect(@service.title).to eq('Jira One')
expect(@service.description).to eq('Jira One issue tracker')
end
@@ -225,7 +225,7 @@ describe JiraService, models: true do
@service.destroy!
end
- it 'should be prepopulated with the settings' do
+ it 'is prepopulated with the settings' do
expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index f37edd4d970..d098d988521 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -39,4 +39,75 @@ describe PivotaltrackerService, models: true do
it { is_expected.not_to validate_presence_of(:token) }
end
end
+
+ describe 'Execute' do
+ let(:service) do
+ PivotaltrackerService.new.tap do |service|
+ service.token = 'secret_api_token'
+ end
+ end
+
+ let(:url) { PivotaltrackerService::API_ENDPOINT }
+
+ def push_data(branch: 'master')
+ {
+ object_kind: 'push',
+ ref: "refs/heads/#{branch}",
+ commits: [
+ {
+ id: '21c12ea',
+ author: {
+ name: 'Some User'
+ },
+ url: 'https://example.com/commit',
+ message: 'commit message',
+ }
+ ]
+ }
+ end
+
+ before do
+ WebMock.stub_request(:post, url)
+ end
+
+ it 'should post correct message' do
+ service.execute(push_data)
+ expect(WebMock).to have_requested(:post, url).with(
+ body: {
+ 'source_commit' => {
+ 'commit_id' => '21c12ea',
+ 'author' => 'Some User',
+ 'url' => 'https://example.com/commit',
+ 'message' => 'commit message'
+ }
+ },
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'X-TrackerToken' => 'secret_api_token'
+ }
+ ).once
+ end
+
+ context 'when allowed branches is specified' do
+ let(:service) do
+ super().tap do |service|
+ service.restrict_to_branch = 'master,v10'
+ end
+ end
+
+ it 'should post message if branch is in the list' do
+ service.execute(push_data(branch: 'master'))
+ service.execute(push_data(branch: 'v10'))
+
+ expect(WebMock).to have_requested(:post, url).twice
+ end
+
+ it 'should not post message if branch is not in the list' do
+ service.execute(push_data(branch: 'mas'))
+ service.execute(push_data(branch: 'v11'))
+
+ expect(WebMock).not_to have_requested(:post, url)
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 555d9757b47..5959c81577d 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -48,7 +48,9 @@ describe PushoverService, models: true do
let(:pushover) { PushoverService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:api_key) { 'verySecret' }
let(:user_key) { 'verySecret' }
@@ -72,7 +74,7 @@ describe PushoverService, models: true do
WebMock.stub_request(:post, api_url)
end
- it 'should call Pushover API' do
+ it 'calls Pushover API' do
pushover.execute(sample_data)
expect(WebMock).to have_requested(:post, api_url).once
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 379c3e1219c..41b93f08050 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -60,6 +60,7 @@ describe SlackService::NoteMessage, models: true do
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 " \
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
index 46dedb66c7c..13aea0b0600 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -47,7 +47,7 @@ describe SlackService::WikiPageMessage, models: true do
context 'when :action == "create"' do
before { args[:object_attributes][:action] = 'create' }
- it 'it returns the attachment for a new wiki page' do
+ it 'returns the attachment for a new wiki page' do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
@@ -60,7 +60,7 @@ describe SlackService::WikiPageMessage, models: true do
context 'when :action == "update"' do
before { args[:object_attributes][:action] = 'update' }
- it 'it returns the attachment for an updated wiki page' do
+ it 'returns the attachment for an updated wiki page' do
expect(subject.attachments).to eq([
{
text: "Wiki page description",
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 155f3e74e0d..28af68d13b4 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -45,7 +45,9 @@ describe SlackService, models: true do
let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
@@ -93,41 +95,42 @@ describe SlackService, models: true do
@wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
- it "should call Slack API for push events" do
+ it "calls Slack API for push events" do
slack.execute(push_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "should call Slack API for issue events" do
+ it "calls Slack API for issue events" do
slack.execute(@issues_sample_data)
expect(WebMock).to have_requested(:post, webhook_url).once
end
- it "should call Slack API for merge requests events" do
+ 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 "should call Slack API for wiki page events" do
+ 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 'should use the username as an option for slack when configured' do
+ 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 'should use the channel as an option when it is configured' do
+ 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).
@@ -136,6 +139,76 @@ describe SlackService, models: true do
)
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
@@ -163,8 +236,8 @@ describe SlackService, models: true do
note: 'a comment on a commit')
end
- it "should call Slack API for commit comment events" do
- data = Gitlab::NoteDataBuilder.build(commit_note, user)
+ 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
@@ -177,8 +250,8 @@ describe SlackService, models: true do
note: "merge request note")
end
- it "should call Slack API for merge request comment events" do
- data = Gitlab::NoteDataBuilder.build(merge_request_note, user)
+ 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
@@ -190,8 +263,8 @@ describe SlackService, models: true do
create(:note_on_issue, project: project, note: "issue note")
end
- it "should call Slack API for issue comment events" do
- data = Gitlab::NoteDataBuilder.build(issue_note, user)
+ 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
@@ -204,8 +277,8 @@ describe SlackService, models: true do
note: "snippet note")
end
- it "should call Slack API for snippet comment events" do
- data = Gitlab::NoteDataBuilder.build(snippet_note, user)
+ 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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5a27ccbab0a..b2baeeb31bb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -23,6 +23,7 @@ describe Project, models: true do
it { is_expected.to have_one(:slack_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_one(:board).dependent(:destroy) }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -69,6 +70,7 @@ describe Project, models: true do
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::ShellAdapter) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
+ it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end
@@ -88,7 +90,7 @@ describe Project, models: true do
it { is_expected.to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:repository_storage) }
- it 'should not allow new projects beyond user limits' do
+ it 'does not allow new projects beyond user limits' do
project2 = build(:project)
allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
expect(project2).not_to be_valid
@@ -97,7 +99,7 @@ describe Project, models: true do
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
- it 'should have an error on the name attribute' do
+ it 'has an error on the name attribute' do
new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
expect(new_project).not_to be_valid
@@ -106,7 +108,7 @@ describe Project, models: true do
end
context "when the new wiki path has been used by the path of other Project" do
- it 'should have an error on the name attribute' do
+ it 'has an error on the name attribute' do
project_with_wiki_suffix = create(:project, path: 'foo.wiki')
new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
@@ -124,23 +126,41 @@ describe Project, models: true do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
- it "should not allow repository storages that don't match a label in the configuration" do
+ it "does not allow repository storages that don't match a label in the configuration" do
expect(project2).not_to be_valid
expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
end
end
- it 'should not allow an invalid URI as import_url' do
+ it 'does not allow an invalid URI as import_url' do
project2 = build(:project, import_url: 'invalid://')
expect(project2).not_to be_valid
end
- it 'should allow a valid URI as import_url' do
+ it 'does allow a valid URI as import_url' do
project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
expect(project2).to be_valid
end
+
+ it 'allows an empty URI' do
+ project2 = build(:project, import_url: '')
+
+ expect(project2).to be_valid
+ end
+
+ it 'does not produce import data on an empty URI' do
+ project2 = build(:project, import_url: '')
+
+ expect(project2.import_data).to be_nil
+ end
+
+ it 'does not produce import data on an invalid URI' do
+ project2 = build(:project, import_url: 'test://')
+
+ expect(project2.import_data).to be_nil
+ end
end
describe 'default_scope' do
@@ -153,12 +173,12 @@ describe Project, models: true do
end
describe 'project token' do
- it 'should set an random token if none provided' do
+ it 'sets an random token if none provided' do
project = FactoryGirl.create :empty_project, runners_token: ''
expect(project.runners_token).not_to eq('')
end
- it 'should not set an random toke if one provided' do
+ it 'does not set an random toke if one provided' do
project = FactoryGirl.create :empty_project, runners_token: 'my-token'
expect(project.runners_token).to eq('my-token')
end
@@ -206,7 +226,7 @@ describe Project, models: true do
end
end
- it 'should return valid url to repo' do
+ it 'returns valid url to repo' do
project = Project.new(path: 'somewhere')
expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
end
@@ -227,12 +247,40 @@ describe Project, models: true do
end
end
+ xdescribe "#new_issue_address" do
+ let(:project) { create(:empty_project, path: "somewhere") }
+ let(:user) { create(:user) }
+
+ context 'incoming email enabled' do
+ before do
+ stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
+ end
+
+ it 'returns the address to create a new issue' do
+ token = user.authentication_token
+ address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab"
+
+ expect(project.new_issue_address(user)).to eq(address)
+ end
+ end
+
+ context 'incoming email disabled' do
+ before do
+ stub_incoming_email_setting(enabled: false)
+ end
+
+ it 'returns nil' do
+ expect(project.new_issue_address(user)).to be_nil
+ end
+ end
+ end
+
describe 'last_activity methods' do
let(:project) { create(:project) }
let(:last_event) { double(created_at: Time.now) }
describe 'last_activity' do
- it 'should alias last_activity to last_event' do
+ it 'alias last_activity to last_event' do
allow(project).to receive(:last_event).and_return(last_event)
expect(project.last_activity).to eq(last_event)
end
@@ -296,20 +344,20 @@ describe Project, models: true do
end
end
- describe :update_merge_requests do
+ describe '#update_merge_requests' do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:key) { create(:key, user_id: project.owner.id) }
let(:prev_commit_id) { merge_request.commits.last.id }
let(:commit_id) { merge_request.commits.first.id }
- it 'should close merge request if last commit from source branch was pushed to target branch' do
+ it 'closes merge request if last commit from source branch was pushed to target branch' do
project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
merge_request.reload
expect(merge_request.merged?).to be_truthy
end
- it 'should update merge request commits with new one if pushed to source branch' do
+ it 'updates merge request commits with new one if pushed to source branch' do
project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
merge_request.reload
expect(merge_request.diff_head_sha).to eq(commit_id)
@@ -345,7 +393,7 @@ describe Project, models: true do
end
end
- describe :to_param do
+ describe '#to_param' do
context 'with namespace' do
before do
@group = create :group, name: 'gitlab'
@@ -354,30 +402,48 @@ describe Project, models: true do
it { expect(@project.to_param).to eq('gitlabhq') }
end
+
+ context 'with invalid path' do
+ it 'returns previous path to keep project suitable for use in URLs when persisted' do
+ project = create(:empty_project, path: 'gitlab')
+ project.path = 'foo&bar'
+
+ expect(project).not_to be_valid
+ expect(project.to_param).to eq 'gitlab'
+ end
+
+ it 'returns current path when new record' do
+ project = build(:empty_project, path: 'gitlab')
+ project.path = 'foo&bar'
+
+ expect(project).not_to be_valid
+ expect(project.to_param).to eq 'foo&bar'
+ end
+ end
end
- describe :repository do
+ describe '#repository' do
let(:project) { create(:project) }
- it 'should return valid repo' do
+ it 'returns valid repo' do
expect(project.repository).to be_kind_of(Repository)
end
end
- describe :default_issues_tracker? do
+ describe '#default_issues_tracker?' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
- it "should be true if used internal tracker" do
+ it "is true if used internal tracker" do
expect(project.default_issues_tracker?).to be_truthy
end
- it "should be false if used other tracker" do
+ it "is false if used other tracker" do
expect(ext_project.default_issues_tracker?).to be_falsey
end
end
- describe :external_issue_tracker do
+ describe '#external_issue_tracker' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
@@ -418,7 +484,7 @@ describe Project, models: true do
end
end
- describe :cache_has_external_issue_tracker do
+ describe '#cache_has_external_issue_tracker' do
let(:project) { create(:project) }
it 'stores true if there is any external_issue_tracker' do
@@ -440,7 +506,58 @@ describe Project, models: true do
end
end
- describe :open_branches do
+ describe '#external_wiki' do
+ let(:project) { create(:project) }
+
+ context 'with an active external wiki' do
+ before do
+ create(:service, project: project, type: 'ExternalWikiService', active: true)
+ project.external_wiki
+ end
+
+ it 'sets :has_external_wiki as true' do
+ expect(project.has_external_wiki).to be(true)
+ end
+
+ it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do
+ expect(project.has_external_wiki).to be(true)
+
+ project.services.external_wikis.first.destroy
+
+ expect(project.has_external_wiki).to be(false)
+ end
+ end
+
+ context 'with an inactive external wiki' do
+ before do
+ create(:service, project: project, type: 'ExternalWikiService', active: false)
+ end
+
+ it 'sets :has_external_wiki as false' do
+ expect(project.has_external_wiki).to be(false)
+ end
+ end
+
+ context 'with no external wiki' do
+ before do
+ project.external_wiki
+ end
+
+ it 'sets :has_external_wiki as false' do
+ expect(project.has_external_wiki).to be(false)
+ end
+
+ it 'sets :has_external_wiki as true if an external wiki service is created later' do
+ expect(project.has_external_wiki).to be(false)
+
+ create(:service, project: project, type: 'ExternalWikiService', active: true)
+
+ expect(project.has_external_wiki).to be(true)
+ end
+ end
+ end
+
+ describe '#open_branches' do
let(:project) { create(:project) }
before do
@@ -517,21 +634,21 @@ describe Project, models: true do
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:project) { create(:project) }
- it 'should be true if avatar is image' do
+ it 'is true if avatar is image' do
project.update_attribute(:avatar, 'uploads/avatar.png')
expect(project.avatar_type).to be_truthy
end
- it 'should be false if avatar is html page' do
+ it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
expect(project.avatar_type).to eq(['only images allowed'])
end
end
- describe :avatar_url do
+ describe '#avatar_url' do
subject { project.avatar_url }
let(:project) { create(:project) }
@@ -568,7 +685,7 @@ describe Project, models: true do
end
end
- describe :pipeline do
+ describe '#pipeline' do
let(:project) { create :project }
let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
@@ -588,7 +705,7 @@ describe Project, models: true do
end
end
- describe :builds_enabled do
+ describe '#builds_enabled' do
let(:project) { create :project }
before { project.builds_enabled = true }
@@ -598,6 +715,20 @@ describe Project, models: true do
it { expect(project.builds_enabled?).to be_truthy }
end
+ describe '.cached_count', caching: true do
+ let(:group) { create(:group, :public) }
+ let!(:project1) { create(:empty_project, :public, group: group) }
+ let!(:project2) { create(:empty_project, :public, group: group) }
+
+ it 'returns total project count' do
+ expect(Project).to receive(:count).once.and_call_original
+
+ 3.times do
+ expect(Project.cached_count).to eq(2)
+ end
+ end
+ end
+
describe '.trending' do
let(:group) { create(:group, :public) }
let(:project1) { create(:empty_project, :public, group: group) }
@@ -690,7 +821,7 @@ describe Project, models: true do
end
end
- describe :any_runners do
+ describe '#any_runners' do
let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
let(:specific_runner) { create(:ci_runner) }
let(:shared_runner) { create(:ci_runner, :shared) }
@@ -698,16 +829,16 @@ describe Project, models: true do
context 'for shared runners disabled' do
let(:shared_runners_enabled) { false }
- it 'there are no runners available' do
+ it 'has no runners available' do
expect(project.any_runners?).to be_falsey
end
- it 'there is a specific runner' do
+ it 'has a specific runner' do
project.runners << specific_runner
expect(project.any_runners?).to be_truthy
end
- it 'there is a shared runner, but they are prohibited to use' do
+ it 'has a shared runner, but they are prohibited to use' do
shared_runner
expect(project.any_runners?).to be_falsey
end
@@ -721,7 +852,7 @@ describe Project, models: true do
context 'for shared runners enabled' do
let(:shared_runners_enabled) { true }
- it 'there is a shared runner' do
+ it 'has a shared runner' do
shared_runner
expect(project.any_runners?).to be_truthy
end
@@ -955,68 +1086,97 @@ describe Project, models: true do
end
describe '#protected_branch?' do
- let(:project) { create(:empty_project) }
+ context 'existing project' do
+ let(:project) { create(:project) }
- it 'returns true when the branch matches a protected branch via direct match' do
- project.protected_branches.create!(name: 'foo')
+ it 'returns true when the branch matches a protected branch via direct match' do
+ create(:protected_branch, project: project, name: "foo")
- expect(project.protected_branch?('foo')).to eq(true)
- end
+ expect(project.protected_branch?('foo')).to eq(true)
+ end
- it 'returns true when the branch matches a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ it 'returns true when the branch matches a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
- expect(project.protected_branch?('production/some-branch')).to eq(true)
- end
+ expect(project.protected_branch?('production/some-branch')).to eq(true)
+ end
- it 'returns false when the branch does not match a protected branch via direct match' do
- expect(project.protected_branch?('foo')).to eq(false)
- end
+ it 'returns false when the branch does not match a protected branch via direct match' do
+ expect(project.protected_branch?('foo')).to eq(false)
+ end
- it 'returns false when the branch does not match a protected branch via wildcard match' do
- project.protected_branches.create!(name: 'production/*')
+ it 'returns false when the branch does not match a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
- expect(project.protected_branch?('staging/some-branch')).to eq(false)
+ expect(project.protected_branch?('staging/some-branch')).to eq(false)
+ end
end
- end
- describe "#developers_can_push_to_protected_branch?" do
- let(:project) { create(:empty_project) }
+ context "new project" do
+ let(:project) { create(:empty_project) }
- context "when the branch matches a protected branch via direct match" do
- it "returns true if 'Developers can Push' is turned on" do
- create(:protected_branch, name: "production", project: project, developers_can_push: true)
+ it 'returns false when default_protected_branch is unprotected' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
- expect(project.developers_can_push_to_protected_branch?('production')).to be true
+ expect(project.protected_branch?('master')).to be false
end
- it "returns false if 'Developers can Push' is turned off" do
- create(:protected_branch, name: "production", project: project, developers_can_push: false)
+ it 'returns false when default_protected_branch lets developers push' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
- expect(project.developers_can_push_to_protected_branch?('production')).to be false
+ expect(project.protected_branch?('master')).to be false
end
- end
- context "when the branch matches a protected branch via wilcard match" do
- it "returns true if 'Developers can Push' is turned on" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+ it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
- expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true
+ expect(project.protected_branch?('master')).to be true
end
- it "returns false if 'Developers can Push' is turned off" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: false)
+ it 'returns true when default_branch_protection is in full protection' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
- expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false
+ expect(project.protected_branch?('master')).to be true
end
end
+ end
- context "when the branch does not match a protected branch" do
- it "returns false" do
- create(:protected_branch, name: "production/*", project: project, developers_can_push: true)
+ describe '#user_can_push_to_empty_repo?' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
- expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false
- end
+ it 'returns false when default_branch_protection is in full protection and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
+ end
+
+ it 'returns false when default_branch_protection only lets devs merge and user is dev' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
+ end
+
+ it 'returns true when default_branch_protection lets devs push and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
+ end
+
+ it 'returns true when default_branch_protection is unprotected and user is developer' do
+ project.team << [user, :developer]
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
+ end
+
+ it 'returns true when user is master' do
+ project.team << [user, :master]
+
+ expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
end
end
@@ -1096,6 +1256,111 @@ describe Project, models: true do
end
end
+ describe '#latest_successful_builds_for' do
+ def create_pipeline(status = 'success')
+ create(:ci_pipeline, project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ def create_build(new_pipeline = pipeline, name = 'test')
+ create(:ci_build, :success, :artifacts,
+ pipeline: new_pipeline,
+ status: new_pipeline.status,
+ name: name)
+ end
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create_pipeline }
+
+ context 'with many builds' do
+ it 'gives the latest builds from latest pipeline' do
+ pipeline1 = create_pipeline
+ pipeline2 = create_pipeline
+ build1_p2 = create_build(pipeline2, 'test')
+ create_build(pipeline1, 'test')
+ create_build(pipeline1, 'test2')
+ build2_p2 = create_build(pipeline2, 'test2')
+
+ latest_builds = project.latest_successful_builds_for
+
+ expect(latest_builds).to contain_exactly(build2_p2, build1_p2)
+ end
+ end
+
+ context 'with succeeded pipeline' do
+ let!(:build) { create_build }
+
+ context 'standalone pipeline' do
+ it 'returns builds for ref for default_branch' do
+ builds = project.latest_successful_builds_for
+
+ expect(builds).to contain_exactly(build)
+ end
+
+ it 'returns empty relation if the build cannot be found' do
+ builds = project.latest_successful_builds_for('TAIL')
+
+ expect(builds).to be_kind_of(ActiveRecord::Relation)
+ expect(builds).to be_empty
+ end
+ end
+
+ context 'with some pending pipeline' do
+ before do
+ create_build(create_pipeline('pending'))
+ end
+
+ it 'gives the latest build from latest pipeline' do
+ latest_build = project.latest_successful_builds_for
+
+ expect(latest_build).to contain_exactly(build)
+ end
+ end
+ end
+
+ context 'with pending pipeline' do
+ before do
+ pipeline.update(status: 'pending')
+ create_build(pipeline)
+ end
+
+ it 'returns empty relation' do
+ builds = project.latest_successful_builds_for
+
+ expect(builds).to be_kind_of(ActiveRecord::Relation)
+ expect(builds).to be_empty
+ end
+ end
+ end
+
+ describe '#add_import_job' do
+ context 'forked' do
+ let(:forked_project_link) { create(:forked_project_link) }
+ let(:forked_from_project) { forked_project_link.forked_from_project }
+ let(:project) { forked_project_link.forked_to_project }
+
+ it 'schedules a RepositoryForkWorker job' do
+ expect(RepositoryForkWorker).to receive(:perform_async).
+ with(project.id, forked_from_project.repository_storage_path,
+ forked_from_project.path_with_namespace, project.namespace.path)
+
+ project.add_import_job
+ end
+ end
+
+ context 'not forked' do
+ let(:project) { create(:project) }
+
+ it 'schedules a RepositoryImportWorker job' do
+ expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
+
+ project.add_import_job
+ end
+ end
+ end
+
describe '.where_paths_in' do
context 'without any paths' do
it 'returns an empty relation' do
@@ -1128,4 +1393,84 @@ describe Project, models: true do
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) }
+
+ it 'calls the before_change_head method' do
+ expect(project.repository).to receive(:before_change_head)
+ project.change_head(project.default_branch)
+ end
+
+ it 'creates the new reference with rugged' do
+ expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+ "refs/heads/#{project.default_branch}",
+ force: true)
+ project.change_head(project.default_branch)
+ end
+
+ it 'copies the gitattributes' do
+ expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'expires the avatar cache' do
+ expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'reloads the default branch' do
+ expect(project).to receive(:reload_default_branch)
+ project.change_head(project.default_branch)
+ end
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 9262aeb6ed8..5eaf0d3b7a6 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -151,8 +151,8 @@ describe ProjectTeam, models: true do
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
- it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
- it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
context 'when project is shared with group' do
@@ -168,14 +168,14 @@ describe ProjectTeam, models: true do
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
- it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
- it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
context 'but share_with_group_lock is true' do
before { project.namespace.update(share_with_group_lock: true) }
- it { expect(project.team.max_member_access(master.id)).to be_nil }
- it { expect(project.team.max_member_access(reporter.id)).to be_nil }
+ it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
end
end
@@ -194,8 +194,74 @@ describe ProjectTeam, models: true do
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) }
it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) }
- it { expect(project.team.max_member_access(nonmember.id)).to be_nil }
- it { expect(project.team.max_member_access(requester.id)).to be_nil }
+ it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) }
+ it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
end
end
+
+ shared_examples_for "#max_member_access_for_users" do |enable_request_store|
+ describe "#max_member_access_for_users" do
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ it 'returns correct roles for different users' do
+ master = create(:user)
+ reporter = create(:user)
+ promoted_guest = create(:user)
+ guest = create(:user)
+ project = create(:project)
+
+ project.team << [master, :master]
+ project.team << [reporter, :reporter]
+ project.team << [promoted_guest, :guest]
+ project.team << [guest, :guest]
+
+ group = create(:group)
+ group_developer = create(:user)
+ second_developer = create(:user)
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER)
+
+ group.add_master(promoted_guest)
+ group.add_developer(group_developer)
+ group.add_developer(second_developer)
+
+ second_group = create(:group)
+ project.project_group_links.create(
+ group: second_group,
+ group_access: Gitlab::Access::MASTER)
+ second_group.add_master(second_developer)
+
+ users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
+
+ expected = {
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER,
+ promoted_guest.id => Gitlab::Access::DEVELOPER,
+ guest.id => Gitlab::Access::GUEST,
+ group_developer.id => Gitlab::Access::DEVELOPER,
+ second_developer.id => Gitlab::Access::MASTER
+ }
+
+ expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ end
+ end
+ end
+
+ describe '#max_member_access_for_users with RequestStore' do
+ it_behaves_like "#max_member_access_for_users", true
+ end
+
+ describe '#max_member_access_for_users without RequestStore' do
+ it_behaves_like "#max_member_access_for_users", false
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 24e49c8def3..1fea50ad42c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -4,19 +4,20 @@ describe Repository, models: true do
include RepoHelpers
TestBlob = Struct.new(:name)
- let(:repository) { create(:project).repository }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
let(:user) { create(:user) }
let(:commit_options) do
author = repository.user_to_committer(user)
{ message: 'Test message', committer: author, author: author }
end
let(:merge_commit) do
- source_sha = repository.find_branch('feature').target
- merge_commit_sha = repository.merge(user, source_sha, 'master', commit_options)
- repository.commit(merge_commit_sha)
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
end
- describe :branch_names_contains do
+ describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
it { is_expected.to include('master') }
@@ -24,7 +25,7 @@ describe Repository, models: true do
it { is_expected.not_to include('fix') }
end
- describe :tag_names_contains do
+ describe '#tag_names_contains' do
subject { repository.tag_names_contains(sample_commit.id) }
it { is_expected.to include('v1.1.0') }
@@ -49,8 +50,9 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now)
double_last = double(committed_date: Time.now - 1.second)
- allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first)
- allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last)
+ allow(tag_a).to receive(:target).and_return(double_first)
+ allow(tag_b).to receive(:target).and_return(double_last)
+ allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
@@ -63,8 +65,9 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now - 1.second)
double_last = double(committed_date: Time.now)
- allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last)
- allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first)
+ allow(tag_a).to receive(:target).and_return(double_last)
+ allow(tag_b).to receive(:target).and_return(double_first)
+ allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
@@ -72,13 +75,13 @@ describe Repository, models: true do
end
end
- describe :last_commit_for_path do
+ describe '#last_commit_for_path' do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
end
- describe :find_commits_by_message do
+ describe '#find_commits_by_message' do
subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
@@ -87,7 +90,7 @@ describe Repository, models: true do
it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
end
- describe :blob_at do
+ describe '#blob_at' do
context 'blank sha' do
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
@@ -95,7 +98,7 @@ describe Repository, models: true do
end
end
- describe :merged_to_root_ref? do
+ describe '#merged_to_root_ref?' do
context 'merged branch' do
subject { repository.merged_to_root_ref?('improve/awesome') }
@@ -103,7 +106,7 @@ describe Repository, models: true do
end
end
- describe :can_be_merged? do
+ describe '#can_be_merged?' do
context 'mergeable branches' do
subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
@@ -129,6 +132,36 @@ describe Repository, models: true do
end
end
+ describe :commit_file do
+ it 'commits change to a file successfully' do
+ expect do
+ repository.commit_file(user, 'CHANGELOG', 'Changelog!',
+ 'Updates file content',
+ 'master', true)
+ end.to change { repository.commits('master').count }.by(1)
+
+ blob = repository.blob_at('master', 'CHANGELOG')
+
+ expect(blob.data).to eq('Changelog!')
+ end
+ end
+
+ describe :update_file do
+ it 'updates filename successfully' do
+ expect do
+ repository.update_file(user, 'NEWLICENSE', 'Copyright!',
+ branch: 'master',
+ previous_path: 'LICENSE',
+ message: 'Changes filename')
+ end.to change { repository.commits('master').count }.by(1)
+
+ files = repository.ls_files('master')
+
+ expect(files).not_to include('LICENSE')
+ expect(files).to include('NEWLICENSE')
+ end
+ end
+
describe "search_files" do
let(:results) { repository.search_files('feature', 'master') }
subject { results }
@@ -305,16 +338,16 @@ describe Repository, models: true do
end
end
- describe :add_branch do
+ describe '#add_branch' do
context 'when pre hooks were successful' do
- it 'should run without errors' do
+ it 'runs without errors' do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
expect { repository.add_branch(user, 'new_feature', 'master') }.not_to raise_error
end
- it 'should create the branch' do
+ it 'creates the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
branch = repository.add_branch(user, 'new_feature', 'master')
@@ -330,7 +363,7 @@ describe Repository, models: true do
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -338,7 +371,7 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
- it 'should not create the branch' do
+ it 'does not create the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -349,15 +382,19 @@ describe Repository, models: true do
end
end
- describe :rm_branch do
+ describe '#rm_branch' do
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:blank_sha) { '0000000000000000000000000000000000000000' }
+
context 'when pre hooks were successful' do
- it 'should run without errors' do
- allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
+ it 'runs without errors' do
+ expect_any_instance_of(GitHooksService).to receive(:execute).
+ with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
- it 'should delete the branch' do
+ it 'deletes the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
@@ -367,7 +404,7 @@ describe Repository, models: true do
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -375,7 +412,7 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
- it 'should not delete the branch' do
+ it 'does not delete the branch' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -386,28 +423,39 @@ describe Repository, models: true do
end
end
- describe :commit_with_hooks do
+ describe '#commit_with_hooks' do
+ let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+
context 'when pre hooks were successful' do
before do
expect_any_instance_of(GitHooksService).to receive(:execute).
- and_return(true)
+ with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature').
+ and_yield.and_return(true)
end
- it 'should run without errors' do
+ it 'runs without errors' do
expect do
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end.not_to raise_error
end
- it 'should ensure the autocrlf Git option is set to :input' do
+ it 'ensures the autocrlf Git option is set to :input' do
expect(repository).to receive(:update_autocrlf_option)
repository.commit_with_hooks(user, 'feature') { sample_commit.id }
end
+
+ context "when the branch wasn't empty" do
+ it 'updates the head' do
+ expect(repository.find_branch('feature').target.id).to eq(old_rev)
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ expect(repository.find_branch('feature').target.id).to eq(sample_commit.id)
+ end
+ end
end
context 'when pre hooks failed' do
- it 'should get an error' do
+ it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
@@ -415,6 +463,43 @@ describe Repository, models: true do
end.to raise_error(GitHooksService::PreReceiveError)
end
end
+
+ context 'when target branch is different from source branch' do
+ before do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ end
+
+ it 'expires branch cache' do
+ expect(repository).not_to receive(:expire_exists_cache)
+ 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.commit_with_hooks(user, 'new-feature') { sample_commit.id }
+ end
+ end
+
+ context 'when repository is empty' do
+ before do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ end
+
+ it 'expires creation and branch cache' do
+ empty_repository = create(:empty_project, :empty_repo).repository
+
+ expect(empty_repository).to receive(:expire_exists_cache)
+ 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)
+ end
+ end
end
describe '#exists?' do
@@ -630,10 +715,18 @@ describe Repository, models: true do
end
describe '#merge' do
- it 'should merge the code and return the commit id' do
+ it 'merges the code and return the commit id' do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
+
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
+
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ end
end
describe '#revert' do
@@ -641,13 +734,13 @@ describe Repository, models: true do
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
context 'when there is a conflict' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
expect(repository.revert(user, new_image_commit, 'master')).to eq(false)
end
end
context 'when commit was already reverted' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
repository.revert(user, update_image_commit, 'master')
expect(repository.revert(user, update_image_commit, 'master')).to eq(false)
@@ -655,13 +748,13 @@ describe Repository, models: true do
end
context 'when commit can be reverted' do
- it 'should revert the changes' do
+ it 'reverts the changes' do
expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
end
end
context 'reverting a merge commit' do
- it 'should revert the changes' do
+ it 'reverts the changes' do
merge_commit
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
@@ -677,13 +770,13 @@ describe Repository, models: true do
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
context 'when there is a conflict' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
end
end
context 'when commit was already cherry-picked' do
- it 'should abort the operation' do
+ it 'aborts the operation' do
repository.cherry_pick(user, pickable_commit, 'master')
expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
@@ -691,13 +784,13 @@ describe Repository, models: true do
end
context 'when commit can be cherry-picked' do
- it 'should cherry-pick the changes' do
+ it 'cherry-picks the changes' do
expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
end
end
context 'cherry-picking a merge commit' do
- it 'should cherry-pick the changes' do
+ it 'cherry-picks the changes' do
expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
repository.cherry_pick(user, pickable_merge, 'master')
@@ -718,6 +811,30 @@ describe Repository, models: true do
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)
@@ -748,6 +865,30 @@ describe Repository, models: true do
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)
@@ -1021,7 +1162,7 @@ describe Repository, models: true do
it 'does not flush the cache if the commit does not change any logos' do
diff = double(:diff, new_path: 'test.txt')
- expect(commit).to receive(:diffs).and_return([diff])
+ expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).not_to receive(:expire)
repository.expire_avatar_cache(repository.root_ref, '123')
@@ -1030,7 +1171,7 @@ describe Repository, models: true do
it 'flushes the cache if the commit changes any of the logos' do
diff = double(:diff, new_path: Repository::AVATAR_FILES[0])
- expect(commit).to receive(:diffs).and_return([diff])
+ expect(commit).to receive(:raw_diffs).and_return([diff])
expect(cache).to receive(:expire).with(:avatar)
repository.expire_avatar_cache(repository.root_ref, '123')
@@ -1082,51 +1223,31 @@ describe Repository, models: true do
end
end
- describe '#local_branches' do
- it 'returns the local branches' do
- masterrev = repository.find_branch('master').target
- create_remote_branch('joe', 'remote_branch', masterrev)
- repository.add_branch(user, 'local_branch', masterrev)
-
- expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
- expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
+ 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
- end
-
- describe '.clean_old_archives' do
- let(:path) { Gitlab.config.gitlab.repository_downloads_path }
- context 'when the downloads directory does not exist' do
- it 'does not remove any archives' do
- expect(File).to receive(:directory?).with(path).and_return(false)
-
- expect(Gitlab::Popen).not_to receive(:popen)
+ it "stores a reference to the specified commit sha so it isn't garbage collected" do
+ repository.keep_around(sample_commit.id)
- described_class.clean_old_archives
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_truthy
end
- context 'when the downloads directory exists' do
- it 'removes old archives' do
- expect(File).to receive(:directory?).with(path).and_return(true)
-
- expect(Gitlab::Popen).to receive(:popen)
+ 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)
- described_class.clean_old_archives
- end
- end
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
- describe "#keep_around" do
- 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
- end
+ expect(repository.kept_around?(sample_commit.id)).to be_falsey
- def create_remote_branch(remote_name, branch_name, target)
- rugged = repository.rugged
- rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target)
+ File.delete(path)
+ end
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 96bbbec9ea1..05056a4bb47 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -22,11 +22,11 @@ describe Service, models: true do
@testable = @service.can_test?
end
- describe :can_test do
+ describe '#can_test?' do
it { expect(@testable).to eq(true) }
end
- describe :test do
+ describe '#test' do
let(:data) { 'test' }
it 'test runs execute' do
@@ -45,7 +45,7 @@ describe Service, models: true do
@testable = @service.can_test?
end
- describe :can_test do
+ describe '#can_test?' do
it { expect(@testable).to eq(true) }
end
end
@@ -65,13 +65,13 @@ describe Service, models: true do
end
let(:project) { create(:project) }
- describe 'should be prefilled for projects pushover service' do
+ describe 'is prefilled for projects pushover service' do
before do
service_template
project.build_missing_services
end
- it "should have all fields prefilled" do
+ it "has all fields prefilled" do
service = project.pushover_service
expect(service.template).to eq(false)
expect(service.device).to eq('MyDevice')
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
new file mode 100644
index 00000000000..a8c25766e73
--- /dev/null
+++ b/spec/models/user_agent_detail_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe UserAgentDetail, type: :model do
+ describe '.submittable?' do
+ it 'is submittable when not already submitted' do
+ detail = build(:user_agent_detail)
+
+ expect(detail.submittable?).to be_truthy
+ end
+
+ it 'is not submittable when already submitted' do
+ detail = build(:user_agent_detail, submitted: true)
+
+ expect(detail.submittable?).to be_falsey
+ end
+ end
+
+ describe '.valid?' do
+ it 'is valid with a subject' do
+ detail = build(:user_agent_detail)
+
+ expect(detail).to be_valid
+ end
+
+ it 'is invalid without a subject' do
+ detail = build(:user_agent_detail, subject: nil)
+
+ expect(detail).not_to be_valid
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3984b30ddf8..8eb0c5033c9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -31,6 +31,8 @@ describe User, models: true do
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
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) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
@@ -87,9 +89,9 @@ describe User, models: true do
end
describe 'email' do
- context 'when no signup domains listed' do
+ context 'when no signup domains whitelisted' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([])
end
it 'accepts any email' do
@@ -98,9 +100,9 @@ describe User, models: true do
end
end
- context 'when a signup domain is listed and subdomains are allowed' do
+ context 'when a signup domain is whitelisted and subdomains are allowed' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com', '*.example.com'])
end
it 'accepts info@example.com' do
@@ -119,9 +121,9 @@ describe User, models: true do
end
end
- context 'when a signup domain is listed and subdomains are not allowed' do
+ context 'when a signup domain is whitelisted and subdomains are not allowed' do
before do
- allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com'])
end
it 'accepts info@example.com' do
@@ -140,6 +142,53 @@ describe User, models: true do
end
end
+ context 'domain blacklist' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true)
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com'])
+ end
+
+ context 'when a signup domain is blacklisted' do
+ it 'accepts info@test.com' do
+ user = build(:user, email: 'info@test.com')
+ expect(user).to be_valid
+ end
+
+ it 'rejects info@example.com' do
+ user = build(:user, email: 'info@example.com')
+ expect(user).not_to be_valid
+ end
+ end
+
+ context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com'])
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com'])
+ end
+
+ it 'gives priority to whitelist and allow info@test.example.com' do
+ user = build(:user, email: 'info@test.example.com')
+ expect(user).to be_valid
+ end
+ end
+
+ context 'with both lists containing a domain' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['test.com'])
+ end
+
+ it 'accepts info@test.com' do
+ user = build(:user, email: 'info@test.com')
+ expect(user).to be_valid
+ end
+
+ it 'rejects info@example.com' do
+ user = build(:user, email: 'info@example.com')
+ expect(user).not_to be_valid
+ end
+ end
+ end
+
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")
@@ -255,18 +304,18 @@ describe User, models: true do
end
describe '#generate_password' do
- it "should execute callback when force_random_password specified" do
+ it "executes callback when force_random_password specified" do
user = build(:user, force_random_password: true)
expect(user).to receive(:generate_password)
user.save
end
- it "should not generate password by default" do
+ it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
- it "should generate password when forcing random password" do
+ it "generates password when forcing random password" do
allow(Devise).to receive(:friendly_token).and_return('123456789')
user = create(:user, password: 'abcdefg', force_random_password: true)
expect(user.password).to eq('12345678')
@@ -274,7 +323,7 @@ describe User, models: true do
end
describe 'authentication token' do
- it "should have authentication token" do
+ it "has authentication token" do
user = create(:user)
expect(user.authentication_token).not_to be_blank
end
@@ -381,7 +430,7 @@ describe User, models: true do
describe 'blocking user' do
let(:user) { create(:user, name: 'John Smith') }
- it "should block user" do
+ it "blocks user" do
user.block
expect(user.blocked?).to be_truthy
end
@@ -427,7 +476,7 @@ describe User, models: true do
end
end
- describe :not_in_project do
+ describe '.not_in_project' do
before do
User.delete_all
@user = create :user
@@ -452,7 +501,7 @@ describe User, models: true do
describe 'with defaults' do
let(:user) { User.new }
- it "should apply defaults to user" do
+ it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
@@ -463,7 +512,7 @@ describe User, models: true do
describe 'with default overrides' do
let(:user) { User.new(projects_limit: 123, can_create_group: false, can_create_team: true, theme_id: 1) }
- it "should apply defaults to user" do
+ it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
expect(user.theme_id).to eq(1)
@@ -553,7 +602,7 @@ describe User, models: true do
describe 'by_username_or_id' do
let(:user1) { create(:user, username: 'foo') }
- it "should get the correct user" do
+ 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
@@ -565,7 +614,7 @@ describe User, models: true do
let(:username) { 'John' }
let!(:user) { create(:user, username: username) }
- it 'should get the correct user' do
+ it 'gets the correct user' do
expect(User.by_login(user.email.upcase)).to eq user
expect(User.by_login(user.email)).to eq user
expect(User.by_login(username.downcase)).to eq user
@@ -590,29 +639,29 @@ describe User, models: true do
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
- it "should have all ssh keys" do
+ it "has all ssh keys" do
user = create :user
key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id
- expect(user.all_ssh_keys).to include(key.key)
+ expect(user.all_ssh_keys).to include(a_string_starting_with(key.key))
end
end
- describe :avatar_type do
+ describe '#avatar_type' do
let(:user) { create(:user) }
- it "should be true if avatar is image" do
+ it "is true if avatar is image" do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
- it "should be false if avatar is html page" do
+ it "is false if avatar is html page" do
user.update_attribute(:avatar, 'uploads/avatar.html')
expect(user.avatar_type).to eq(["only images allowed"])
end
end
- describe :requires_ldap_check? do
+ describe '#requires_ldap_check?' do
let(:user) { User.new }
it 'is false when LDAP is disabled' do
@@ -651,7 +700,7 @@ describe User, models: true do
end
context 'ldap synchronized user' do
- describe :ldap_user? do
+ describe '#ldap_user?' do
it 'is true if provider name starts with ldap' do
user = create(:omniauth_user, provider: 'ldapmain')
expect(user.ldap_user?).to be_truthy
@@ -668,7 +717,7 @@ describe User, models: true do
end
end
- describe :ldap_identity do
+ describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
expect(user.ldap_identity.provider).not_to be_empty
@@ -825,7 +874,7 @@ describe User, models: true do
end
end
- describe :can_be_removed? do
+ describe '#can_be_removed?' do
subject { create(:user) }
context 'no owned groups' do
@@ -846,7 +895,9 @@ describe User, models: true do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project1) }
- let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) }
+ let!(:push_data) do
+ Gitlab::DataBuilder::Push.build_sample(project2, subject)
+ end
let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) }
before do
@@ -869,6 +920,16 @@ describe User, models: true do
expect(subject.recent_push).to eq(nil)
end
+
+ it "includes push events on any of the provided projects" do
+ expect(subject.recent_push(project1)).to eq(nil)
+ expect(subject.recent_push(project2)).to eq(push_event)
+
+ push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
+ push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+
+ expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
+ end
end
describe '#authorized_groups' do
@@ -885,16 +946,72 @@ describe User, models: true do
end
describe '#authorized_projects' do
- let!(:user) { create(:user) }
- let!(:private_project) { create(:project, :private) }
+ context 'with a minimum access level' do
+ it 'includes projects for which the user is an owner' do
+ user = create(:user)
+ project = create(:empty_project, :private, namespace: user.namespace)
- before do
- private_project.team << [user, Gitlab::Access::MASTER]
+ expect(user.authorized_projects(Gitlab::Access::REPORTER))
+ .to contain_exactly(project)
+ end
+
+ it 'includes projects for which the user is a master' do
+ user = create(:user)
+ project = create(:empty_project, :private)
+
+ project.team << [user, Gitlab::Access::MASTER]
+
+ expect(user.authorized_projects(Gitlab::Access::REPORTER))
+ .to contain_exactly(project)
+ end
end
+ end
+
+ describe '#projects_where_can_admin_issues' do
+ let(:user) { create(:user) }
- subject { user.authorized_projects }
+ it 'includes projects for which the user access level is above or equal to reporter' do
+ create(:project)
+ reporter_project = create(:project)
+ developer_project = create(:project)
+ master_project = create(:project)
- it { is_expected.to eq([private_project]) }
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
+ expect(user.can?(:admin_issue, master_project)).to eq(true)
+ expect(user.can?(:admin_issue, developer_project)).to eq(true)
+ expect(user.can?(:admin_issue, reporter_project)).to eq(true)
+ end
+
+ it 'does not include for which the user access level is below reporter' do
+ project = create(:project)
+ guest_project = create(:project)
+
+ guest_project.team << [user, :guest]
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, guest_project)).to eq(false)
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
+
+ it 'does not include archived projects' do
+ project = create(:project)
+ project.update_attributes(archived: true)
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
+
+ it 'does not include projects for which issues are disabled' do
+ project = create(:project)
+ project.update_attributes(issues_enabled: false)
+
+ expect(user.projects_where_can_admin_issues.to_a).to be_empty
+ expect(user.can?(:admin_issue, project)).to eq(false)
+ end
end
describe '#ci_authorized_runners' do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index ddc49495eda..5c34b1b0a30 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -147,12 +147,12 @@ describe WikiPage, models: true do
@page = wiki.find_page("Delete Page")
end
- it "should delete the page" do
+ it "deletes the page" do
@page.delete
expect(wiki.pages).to be_empty
end
- it "should return true" do
+ it "returns true" do
expect(@page.delete).to eq(true)
end
end
@@ -183,7 +183,7 @@ describe WikiPage, models: true do
destroy_page("Title")
end
- it "should be replace a hyphen to a space" do
+ it "replaces a hyphen to a space" do
@page.title = "Import-existing-repositories-into-GitLab"
expect(@page.title).to eq("Import existing repositories into GitLab")
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
new file mode 100644
index 00000000000..d78494b76fa
--- /dev/null
+++ b/spec/requests/api/access_requests_spec.rb
@@ -0,0 +1,246 @@
+require 'spec_helper'
+
+describe API::AccessRequests, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ 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
+ end
+
+ let(:group) do
+ group = create(:group, :public)
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group
+ end
+
+ shared_examples 'GET /:sources/:id/access_requests' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns access requesters' do
+ get api("/#{source_type.pluralize}/#{source.id}/access_requests", master)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/access_requests' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger) }
+ end
+
+ context 'when authenticated as a member' do
+ %i[developer master].each do |type|
+ context "as a #{type}" do
+ it 'returns 400' do
+ expect do
+ user = public_send(type)
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
+
+ expect(response).to have_http_status(400)
+ end.not_to change { source.requesters.count }
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as an access requester' do
+ it 'returns 400' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", access_requester)
+
+ expect(response).to have_http_status(400)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ context 'when authenticated as a stranger' do
+ it 'returns 201' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+ expect(response).to have_http_status(201)
+ end.to change { source.requesters.count }.by(1)
+
+ # User attributes
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['name']).to eq(stranger.name)
+ expect(json_response['username']).to eq(stranger.username)
+ expect(json_response['state']).to eq(stranger.state)
+ expect(json_response['avatar_url']).to eq(stranger.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(stranger))
+
+ # Member attributes
+ expect(json_response['requested_at']).to be_present
+ end
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/access_requests/:user_id/approve' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns 201' do
+ expect do
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}/approve", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ # User attributes
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['name']).to eq(access_requester.name)
+ expect(json_response['username']).to eq(access_requester.username)
+ expect(json_response['state']).to eq(access_requester.state)
+ expect(json_response['avatar_url']).to eq(access_requester.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(access_requester))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+
+ context 'user_id does not match an existing access requester' do
+ it 'returns 404' do
+ expect do
+ put api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}/approve", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.members.count }
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/access_requests/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-master/owner' do
+ %i[developer stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as the access requester' do
+ it 'returns 200' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.requesters.count }.by(-1)
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'returns 200' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.requesters.count }.by(-1)
+ end
+
+ context 'user_id does not match an existing access requester' do
+ it 'returns 404' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{stranger.id}", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/access_requests', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/access_requests', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/access_requests', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/access_requests', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/access_requests/:user_id/approve', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/access_requests/:user_id', 'group' do
+ let(:source) { group }
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 83025953889..bbdf8f03c2b 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::Helpers, api: true do
include API::Helpers
include ApiHelpers
+ include SentryHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -41,19 +42,19 @@ describe API::Helpers, api: true do
describe ".current_user" do
describe "when authenticating using a user's private token" do
- it "should return nil for an invalid 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 "should return nil for a user without access" do
+ it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
- it "should leave user as is when sudo not specified" do
+ 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
@@ -65,19 +66,19 @@ describe API::Helpers, api: true do
describe "when authenticating using a user's personal access tokens" do
let(:personal_access_token) { create(:personal_access_token, user: user) }
- it "should return nil for an invalid 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 "should return nil for a user without access" do
+ it "returns nil for a user without access" do
env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
- allow(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+ allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
expect(current_user).to be_nil
end
- it "should leave user as is when sudo not specified" do
+ 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
@@ -100,7 +101,7 @@ describe API::Helpers, api: true do
end
end
- it "should change current user to sudo when admin" do
+ 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)
@@ -111,7 +112,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
- it "should throw an error when the current user is not an admin and attempting to sudo" do
+ 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)
@@ -122,7 +123,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should throw an error when the user cannot be found for a given id" do
+ 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)
@@ -133,7 +134,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should throw an error when the user cannot be found for a given username" do
+ 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)
@@ -144,7 +145,7 @@ describe API::Helpers, api: true do
expect { current_user }.to raise_error(Exception)
end
- it "should handle sudo's to oneself" do
+ it "handles sudo's to oneself" do
set_env(admin, admin.id)
expect(current_user).to eq(admin)
set_param(admin, admin.id)
@@ -155,7 +156,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(admin)
end
- it "should handle multiple sudo's to oneself" do
+ 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)
@@ -171,7 +172,7 @@ describe API::Helpers, api: true do
expect(current_user).to eq(user)
end
- it "should handle multiple sudo's to oneself using string ids" do
+ 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)
@@ -183,7 +184,7 @@ describe API::Helpers, api: true do
end
describe '.sudo_identifier' do
- it "should return integers when input is an int" do
+ it "returns integers when input is an int" do
set_env(admin, '123')
expect(sudo_identifier).to eq(123)
set_env(admin, '0001234567890')
@@ -195,7 +196,7 @@ describe API::Helpers, api: true do
expect(sudo_identifier).to eq(1234567890)
end
- it "should return string when input is an is not an int" do
+ 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')
@@ -211,4 +212,53 @@ describe API::Helpers, api: true do
expect(sudo_identifier).to eq(' 123')
end
end
+
+ describe '.to_boolean' do
+ it 'converts a valid string to a boolean' do
+ expect(to_boolean('true')).to be_truthy
+ expect(to_boolean('YeS')).to be_truthy
+ expect(to_boolean('t')).to be_truthy
+ expect(to_boolean('1')).to be_truthy
+ expect(to_boolean('ON')).to be_truthy
+ expect(to_boolean('FaLse')).to be_falsy
+ expect(to_boolean('F')).to be_falsy
+ expect(to_boolean('NO')).to be_falsy
+ expect(to_boolean('n')).to be_falsy
+ expect(to_boolean('0')).to be_falsy
+ expect(to_boolean('oFF')).to be_falsy
+ end
+
+ it 'converts an invalid string to nil' do
+ expect(to_boolean('fals')).to be_nil
+ expect(to_boolean('yeah')).to be_nil
+ expect(to_boolean('')).to be_nil
+ expect(to_boolean(nil)).to be_nil
+ 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/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 72a6d45f47d..73c268c0d1e 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -22,7 +22,7 @@ describe API::API, api: true do
expect(json_response.first['name']).to eq(award_emoji.name)
end
- it "should return a 404 error when issue id not found" do
+ it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/award_emoji", user)
expect(response).to have_http_status(404)
@@ -124,17 +124,33 @@ describe API::API, api: true do
expect(json_response['user']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if the name is not given" do
+ it "returns a 400 bad request error if the name is not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if the user is not authenticated" do
+ it "returns a 401 unauthorized error if the user is not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji"), name: 'thumbsup'
expect(response).to have_http_status(401)
end
+
+ it "normalizes +1 as thumbsup award" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
+
+ expect(issue.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+ post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
end
end
@@ -147,6 +163,22 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
expect(json_response['user']['username']).to eq(user.username)
end
+
+ it "normalizes +1 as thumbsup award" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
+
+ expect(note.award_emoji.last.name).to eq("thumbsup")
+ end
+
+ context 'when the emoji already has been awarded' do
+ it 'returns a 404 status code' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
+
+ expect(response).to have_http_status(404)
+ expect(json_response["message"]).to match("has already been taken")
+ end
+ end
end
describe 'DELETE /projects/:id/awardable/:awardable_id/award_emoji/:award_id' do
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index b11ca26ee68..3fd989dd7a6 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true do
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
describe "GET /projects/:id/repository/branches" do
- it "should return an array of project branches" do
+ it "returns an array of project branches" do
project.repository.expire_cache
get api("/projects/#{project.id}/repository/branches", user)
@@ -25,47 +25,130 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/branches/:branch" do
- it "should return the branch information for a single branch" do
+ it "returns the branch information for a single branch" do
get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
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 "should return a 403 error if guest" do
+ it "returns a 403 error if guest" do
get api("/projects/#{project.id}/repository/branches", user2)
expect(response).to have_http_status(403)
end
- it "should return a 404 error if branch is not available" do
+ it "returns a 404 error if branch is not available" do
get api("/projects/#{project.id}/repository/branches/unknown", user)
expect(response).to have_http_status(404)
end
end
- describe "PUT /projects/:id/repository/branches/:branch/protect" do
- it "should protect a single branch" do
+ describe 'PUT /projects/:id/repository/branches/:branch/protect' do
+ it 'protects a single branch' do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
+
expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ 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
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+ it 'protects a single branch and developers can merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_merge: true
+
+ expect(response).to have_http_status(200)
expect(json_response['name']).to eq(branch_name)
expect(json_response['commit']['id']).to eq(branch_sha)
expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+
+ it 'protects a single branch and developers can push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: true, developers_can_merge: true
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
+
+ it 'protects a single branch and developers cannot push and merge' do
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
+ developers_can_push: 'tru', developers_can_merge: 'tr'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ context 'on a protected branch' do
+ let(:protected_branch) { 'foo' }
+
+ before do
+ project.repository.add_branch(user, protected_branch, 'master')
+ create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch)
+ end
+
+ it 'updates that a developer can push' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
+ developers_can_push: false, developers_can_merge: false
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+ end
+
+ it 'does not update that a developer can push' do
+ put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user),
+ developers_can_push: 'foobar', developers_can_merge: 'foo'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq(protected_branch)
+ expect(json_response['protected']).to eq(true)
+ expect(json_response['developers_can_push']).to eq(true)
+ expect(json_response['developers_can_merge']).to eq(true)
+ end
end
- it "should return a 404 error if branch not found" do
+ it "returns a 404 error if branch not found" do
put api("/projects/#{project.id}/repository/branches/unknown/protect", user)
expect(response).to have_http_status(404)
end
- it "should return a 403 error if guest" do
+ it "returns a 403 error if guest" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
expect(response).to have_http_status(403)
end
- it "should return success when protect branch again" do
+ it "returns success when protect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user)
expect(response).to have_http_status(200)
@@ -73,7 +156,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/repository/branches/:branch/unprotect" do
- it "should unprotect a single branch" do
+ it "unprotects a single branch" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
expect(response).to have_http_status(200)
@@ -82,12 +165,12 @@ describe API::API, api: true do
expect(json_response['protected']).to eq(false)
end
- it "should return success when unprotect branch" do
+ it "returns success when unprotect branch" do
put api("/projects/#{project.id}/repository/branches/unknown/unprotect", user)
expect(response).to have_http_status(404)
end
- it "should return success when unprotect branch again" do
+ it "returns success when unprotect branch again" do
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
put api("/projects/#{project.id}/repository/branches/#{branch_name}/unprotect", user)
expect(response).to have_http_status(200)
@@ -95,7 +178,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/repository/branches" do
- it "should create a new branch" do
+ it "creates a new branch" do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'feature1',
ref: branch_sha
@@ -106,14 +189,14 @@ describe API::API, api: true do
expect(json_response['commit']['id']).to eq(branch_sha)
end
- it "should deny for user without push access" do
+ it "denies for user without push access" do
post api("/projects/#{project.id}/repository/branches", user2),
branch_name: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
end
- it 'should return 400 if branch name is invalid' do
+ it 'returns 400 if branch name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new design',
ref: branch_sha
@@ -121,7 +204,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch name is invalid')
end
- it 'should return 400 if branch already exists' do
+ it 'returns 400 if branch already exists' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design1',
ref: branch_sha
@@ -134,7 +217,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch already exists')
end
- it 'should return 400 if ref name is invalid' do
+ it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/branches", user),
branch_name: 'new_design3',
ref: 'foo'
@@ -148,25 +231,25 @@ describe API::API, api: true do
allow_any_instance_of(Repository).to receive(:rm_branch).and_return(true)
end
- it "should remove branch" do
+ it "removes branch" do
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(200)
expect(json_response['branch_name']).to eq(branch_name)
end
- it 'should return 404 if branch not exists' do
+ it 'returns 404 if branch not exists' do
delete api("/projects/#{project.id}/repository/branches/foobar", user)
expect(response).to have_http_status(404)
end
- it "should remove protected branch" do
- project.protected_branches.create(name: branch_name)
+ it "removes protected branch" do
+ create(:protected_branch, project: project, name: branch_name)
delete api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Protected branch cant be removed')
end
- it "should not remove HEAD branch" do
+ it "does not remove HEAD branch" do
delete api("/projects/#{project.id}/repository/branches/master", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('Cannot remove HEAD branch')
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index f5b39c3d698..9a17a705b1e 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:api_user) { user }
- let(:user2) { create(:user) }
let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
@@ -18,7 +18,7 @@ describe API::API, api: true do
before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
context 'authorized user' do
- it 'should return project builds' do
+ it 'returns project builds' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
end
@@ -84,7 +84,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user)
end
- it 'should return project builds for specific commit' do
+ it 'returns project builds for specific commit' do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
@@ -113,7 +113,7 @@ describe API::API, api: true do
get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil)
end
- it 'should not return project builds' do
+ it 'does not return project builds' do
expect(response).to have_http_status(401)
expect(json_response.except('message')).to be_empty
end
@@ -125,7 +125,7 @@ describe API::API, api: true do
before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
context 'authorized user' do
- it 'should return specific build data' do
+ it 'returns specific build data' do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
@@ -134,7 +134,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build data' do
+ it 'does not return specific build data' do
expect(response).to have_http_status(401)
end
end
@@ -152,7 +152,7 @@ describe API::API, api: true do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'should return specific build artifacts' do
+ it 'returns specific build artifacts' do
expect(response).to have_http_status(200)
expect(response.headers).to include(download_headers)
end
@@ -161,24 +161,122 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build artifacts' do
+ it 'does not return specific build artifacts' do
expect(response).to have_http_status(401)
end
end
end
- it 'should not return build artifacts if not uploaded' do
+ it 'does not return build artifacts if not uploaded' do
expect(response).to have_http_status(404)
end
end
+ describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
+ let(:api_user) { reporter.user }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+
+ before do
+ build.success
+ end
+
+ def path_for_ref(ref = pipeline.ref, job = build.name)
+ api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user)
+ end
+
+ context 'when not logged in' do
+ let(:api_user) { nil }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when logging as guest' do
+ let(:api_user) { guest.user }
+
+ before do
+ get path_for_ref
+ end
+
+ it 'gives 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'non-existing build' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get path_for_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such build' do
+ before do
+ get path_for_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'find proper build' do
+ shared_examples 'a valid file' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get path_for_ref('master')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+ end
+
+ before do
+ get path_for_ref('improve/awesome')
+ end
+
+ it_behaves_like 'a valid file'
+ end
+ end
+ end
+
describe 'GET /projects/:id/builds/:build_id/trace' do
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
- before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user)
+ end
context 'authorized user' do
- it 'should return specific build trace' do
+ it 'returns specific build trace' do
expect(response).to have_http_status(200)
expect(response.body).to eq(build.trace)
end
@@ -187,7 +285,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not return specific build trace' do
+ it 'does not return specific build trace' do
expect(response).to have_http_status(401)
end
end
@@ -198,16 +296,16 @@ describe API::API, api: true do
context 'authorized user' do
context 'user with :update_build persmission' do
- it 'should cancel running or pending build' do
+ it 'cancels running or pending build' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
end
end
context 'user without :update_build permission' do
- let(:api_user) { user2 }
+ let(:api_user) { reporter.user }
- it 'should not cancel build' do
+ it 'does not cancel build' do
expect(response).to have_http_status(403)
end
end
@@ -216,7 +314,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not cancel build' do
+ it 'does not cancel build' do
expect(response).to have_http_status(401)
end
end
@@ -229,7 +327,7 @@ describe API::API, api: true do
context 'authorized user' do
context 'user with :update_build permission' do
- it 'should retry non-running build' do
+ it 'retries non-running build' do
expect(response).to have_http_status(201)
expect(project.builds.first.status).to eq('canceled')
expect(json_response['status']).to eq('pending')
@@ -237,9 +335,9 @@ describe API::API, api: true do
end
context 'user without :update_build permission' do
- let(:api_user) { user2 }
+ let(:api_user) { reporter.user }
- it 'should not retry build' do
+ it 'does not retry build' do
expect(response).to have_http_status(403)
end
end
@@ -248,7 +346,7 @@ describe API::API, api: true do
context 'unauthorized user' do
let(:api_user) { nil }
- it 'should not retry build' do
+ it 'does not retry build' do
expect(response).to have_http_status(401)
end
end
@@ -262,14 +360,14 @@ describe API::API, api: true do
context 'build is erasable' do
let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) }
- it 'should erase build content' do
+ it 'erases build content' do
expect(response.status).to eq 201
expect(build.trace).to be_empty
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
end
- it 'should update build' do
+ it 'updates build' do
expect(build.reload.erased_at).to be_truthy
expect(build.reload.erased_by).to eq user
end
@@ -278,7 +376,7 @@ describe API::API, api: true do
context 'build is not erasable' do
let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
@@ -309,4 +407,27 @@ describe API::API, api: true do
end
end
end
+
+ describe 'POST /projects/:id/builds/:build_id/play' do
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/play", user)
+ end
+
+ context 'on an playable build' do
+ let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) }
+
+ it 'plays the build' do
+ expect(response).to have_http_status 200
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['id']).to eq(build.id)
+ end
+ end
+
+ context 'on a non-playable build' do
+ it 'returns a status code 400, Bad Request' do
+ expect(response).to have_http_status 400
+ expect(response.body).to match("Unplayable Build")
+ end
+ end
+ end
end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 2da01da7fa1..2d6093fec7a 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -99,7 +99,7 @@ describe API::CommitStatuses, api: true do
context "guest user" do
before { get api(get_url, guest) }
- it "should not return project commits" do
+ it "does not return project commits" do
expect(response).to have_http_status(403)
end
end
@@ -107,7 +107,7 @@ describe API::CommitStatuses, api: true do
context "unauthorized user" do
before { get api(get_url) }
- it "should not return project commits" do
+ it "does not return project commits" do
expect(response).to have_http_status(401)
end
end
@@ -179,7 +179,7 @@ describe API::CommitStatuses, api: true do
context 'reporter user' do
before { post api(post_url, reporter) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(403)
end
end
@@ -187,7 +187,7 @@ describe API::CommitStatuses, api: true do
context 'guest user' do
before { post api(post_url, guest) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(403)
end
end
@@ -195,7 +195,7 @@ describe API::CommitStatuses, api: true do
context 'unauthorized user' do
before { post api(post_url) }
- it 'should not create commit status' do
+ it 'does not create commit status' do
expect(response).to have_http_status(401)
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5219c808791..7ca75d77673 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return project commits" do
+ it "returns project commits" do
get api("/projects/#{project.id}/repository/commits", user)
expect(response).to have_http_status(200)
@@ -27,14 +27,14 @@ describe API::API, api: true do
end
context "unauthorized user" do
- it "should not return project commits" do
+ it "does not return project commits" do
get api("/projects/#{project.id}/repository/commits")
expect(response).to have_http_status(401)
end
end
context "since optional parameter" do
- it "should return project commits since provided parameter" do
+ it "returns project commits since provided parameter" do
commits = project.repository.commits("master")
since = commits.second.created_at
@@ -47,7 +47,7 @@ describe API::API, api: true do
end
context "until optional parameter" do
- it "should return project commits until provided parameter" do
+ it "returns project commits until provided parameter" do
commits = project.repository.commits("master")
before = commits.second.created_at
@@ -60,7 +60,7 @@ describe API::API, api: true do
end
context "invalid xmlschema date parameters" do
- it "should return an invalid parameter error message" do
+ it "returns an invalid parameter error message" do
get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
expect(response).to have_http_status(400)
@@ -71,34 +71,51 @@ describe API::API, api: true do
describe "GET /projects:id/repository/commits/:sha" do
context "authorized user" do
- it "should return a commit by sha" do
+ it "returns a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(project.repository.commit.id)
expect(json_response['title']).to eq(project.repository.commit.title)
+ expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions)
+ expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions)
+ expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total)
end
- it "should return a 404 error if not found" do
+ it "returns a 404 error if not found" do
get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
expect(response).to have_http_status(404)
end
- it "should return nil for commit without CI" do
+ it "returns nil for commit without CI" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['status']).to be_nil
end
- it "should return status for CI" do
+ it "returns status for CI" do
pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+ pipeline.update(status: 'success')
+
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
expect(response).to have_http_status(200)
expect(json_response['status']).to eq(pipeline.status)
end
+
+ it "returns status for CI when pipeline is created" do
+ project.ensure_pipeline(project.repository.commit.sha, 'master')
+
+ get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to be_nil
+ end
end
context "unauthorized user" do
- it "should not return the selected commit" do
+ it "does not return the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
expect(response).to have_http_status(401)
end
@@ -109,7 +126,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return the diff of the selected commit" do
+ it "returns the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
expect(response).to have_http_status(200)
@@ -118,14 +135,14 @@ describe API::API, api: true do
expect(json_response.first.keys).to include "diff"
end
- it "should return a 404 error if invalid commit" do
+ it "returns a 404 error if invalid commit" do
get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
- it "should not return the diff of the selected commit" do
+ it "does not return the diff of the selected commit" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
expect(response).to have_http_status(401)
end
@@ -134,7 +151,7 @@ describe API::API, api: true do
describe 'GET /projects:id/repository/commits/:sha/comments' do
context 'authorized user' do
- it 'should return merge_request comments' do
+ it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -143,14 +160,14 @@ describe API::API, api: true do
expect(json_response.first['author']['id']).to eq(user.id)
end
- it 'should return a 404 error if merge_request_id not found' do
+ it 'returns a 404 error if merge_request_id not found' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
- it 'should not return the diff of the selected commit' do
+ it 'does not return the diff of the selected commit' do
get api("/projects/#{project.id}/repository/commits/1234ab/comments")
expect(response).to have_http_status(401)
end
@@ -159,7 +176,7 @@ describe API::API, api: true do
describe 'POST /projects:id/repository/commits/:sha/comments' do
context 'authorized user' do
- it 'should return comment' do
+ it 'returns comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
@@ -168,28 +185,28 @@ describe API::API, api: true do
expect(json_response['line_type']).to be_nil
end
- it 'should return the inline comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.diffs.first.new_path, line: 7, line_type: 'new'
+ it 'returns the inline comment' do
+ post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 7, line_type: 'new'
expect(response).to have_http_status(201)
expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to eq(project.repository.commit.diffs.first.new_path)
+ expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
expect(json_response['line']).to eq(7)
expect(json_response['line_type']).to eq('new')
end
- it 'should return 400 if note is missing' do
+ it 'returns 400 if note is missing' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
expect(response).to have_http_status(400)
end
- it 'should return 404 if note is attached to non existent commit' do
+ it 'returns 404 if note is attached to non existent commit' do
post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
expect(response).to have_http_status(404)
end
end
context 'unauthorized user' do
- it 'should not return the diff of the selected commit' do
+ it 'does not return the diff of the selected commit' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
expect(response).to have_http_status(401)
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
new file mode 100644
index 00000000000..7d8cc45327c
--- /dev/null
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project, creator_id: user.id) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ describe 'GET /deploy_keys' do
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get api('/deploy_keys')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 403 error' do
+ get api('/deploy_keys', user)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'when authenticated as admin' do
+ it 'should return all deploy keys' do
+ get api('/deploy_keys', admin)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deploy_keys' do
+ before { deploy_key }
+
+ it 'should return array of ssh keys' do
+ get api("/projects/#{project.id}/deploy_keys", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['title']).to eq(deploy_key.title)
+ end
+ end
+
+ describe 'GET /projects/:id/deploy_keys/:key_id' do
+ it 'should return a single key' do
+ get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(deploy_key.title)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ get api("/projects/#{project.id}/deploy_keys/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/deploy_keys' do
+ it 'should not create an invalid ssh key' do
+ post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' }
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['key']).to eq([
+ 'can\'t be blank',
+ 'is too short (minimum is 0 characters)',
+ 'is invalid'
+ ])
+ end
+
+ it 'should not create a key without title' do
+ post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'can\'t be blank',
+ 'is too short (minimum is 0 characters)'
+ ])
+ end
+
+ it 'should create new ssh key' do
+ key_attrs = attributes_for :another_key
+
+ expect do
+ post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
+ end.to change{ project.deploy_keys.count }.by(1)
+ end
+ end
+
+ describe 'DELETE /projects/:id/deploy_keys/:key_id' do
+ before { deploy_key }
+
+ it 'should delete existing key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin)
+ end.to change{ project.deploy_keys.count }.by(-1)
+ end
+
+ it 'should return 404 Not Found with invalid ID' do
+ delete api("/projects/#{project.id}/deploy_keys/404", admin)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'POST /projects/:id/deploy_keys/:key_id/enable' do
+ let(:project2) { create(:empty_project) }
+
+ context 'when the user can admin the project' do
+ it 'enables the key' do
+ expect do
+ post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", admin)
+ end.to change { project2.deploy_keys.count }.from(0).to(1)
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/deploy_keys/:key_id/disable' do
+ context 'when the user can admin the project' do
+ it 'disables the key' do
+ expect do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", admin)
+ end.to change { project.deploy_keys.count }.from(1).to(0)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(deploy_key.id)
+ end
+ end
+
+ context 'when authenticated as non-admin user' do
+ it 'should return a 404 error' do
+ delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}/disable", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb
new file mode 100644
index 00000000000..8fa8c66db6c
--- /dev/null
+++ b/spec/requests/api/deployments_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { deployment.environment.project }
+ let!(:deployment) { create(:deployment) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/deployments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/deployments", user) }
+ end
+
+ it 'returns projects deployments' do
+ get api("/projects/#{project.id}/deployments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['iid']).to eq(deployment.iid)
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/deployments/:deployment_id' do
+ context 'as a member of the project' do
+ it 'returns the projects deployment' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ expect(json_response['id']).to eq(deployment.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
new file mode 100644
index 00000000000..1898b07835d
--- /dev/null
+++ b/spec/requests/api/environments_spec.rb
@@ -0,0 +1,130 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, :private, namespace: user.namespace) }
+ let!(:environment) { create(:environment, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/environments' do
+ context 'as member of the project' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/environments", user) }
+ end
+
+ it 'returns project environments' do
+ get api("/projects/#{project.id}/environments", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['name']).to eq(environment.name)
+ expect(json_response.first['external_url']).to eq(environment.external_url)
+ expect(json_response.first['project']['id']).to eq(project.id)
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/environments' do
+ context 'as a member' do
+ it 'creates a environment with valid params' do
+ post api("/projects/#{project.id}/environments", user), name: "mepmep"
+
+ expect(response).to have_http_status(201)
+ expect(json_response['name']).to eq('mepmep')
+ expect(json_response['external']).to be nil
+ end
+
+ it 'requires name to be passed' do
+ post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com'
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns a 400 if environment already exists' do
+ post api("/projects/#{project.id}/environments", user), name: environment.name
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com'
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns a 400 when the required params are missing' do
+ post api("/projects/12345/environments", non_member), external_url: 'http://env.git.com'
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/environments/:environment_id' do
+ it 'returns a 200 if name and external_url are changed' do
+ url = 'https://mepmep.whatever.ninja'
+ put api("/projects/#{project.id}/environments/#{environment.id}", user),
+ name: 'Mepmep', external_url: url
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ 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),
+ name: 'Mepmep'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).to eq('Mepmep')
+ expect(json_response['external_url']).to eq(url)
+ end
+
+ it 'returns a 404 if the environment does not exist' do
+ put api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'DELETE /projects/:id/environments/:environment_id' do
+ context 'as a master' do
+ it 'returns a 200 for an existing environment' do
+ delete api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns a 404 for non existing id' do
+ delete api("/projects/#{project.id}/environments/12345", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'a non member' do
+ it 'rejects the request' do
+ delete api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 2e5448143d5..2d1213df8a7 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -9,7 +9,7 @@ describe API::API, api: true do
before { project.team << [user, :developer] }
describe "GET /projects/:id/repository/files" do
- it "should return file info" do
+ it "returns file info" do
params = {
file_path: file_path,
ref: 'master',
@@ -23,12 +23,12 @@ describe API::API, api: true do
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
get api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 404 if such file does not exist" do
+ it "returns a 404 if such file does not exist" do
params = {
file_path: 'app/models/application.rb',
ref: 'master',
@@ -49,18 +49,18 @@ describe API::API, api: true do
}
end
- it "should create a new file in project repo" do
+ it "creates a new file in project repo" do
post api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
post api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 400 if editor fails to create file" do
+ it "returns a 400 if editor fails to create file" do
allow_any_instance_of(Repository).to receive(:commit_file).
and_return(false)
@@ -79,13 +79,13 @@ describe API::API, api: true do
}
end
- it "should update existing file in project repo" do
+ it "updates existing file in project repo" do
put api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
put api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
@@ -100,18 +100,18 @@ describe API::API, api: true do
}
end
- it "should delete existing file in project repo" do
+ it "deletes existing file in project repo" do
delete api("/projects/#{project.id}/repository/files", user), valid_params
expect(response).to have_http_status(200)
expect(json_response['file_path']).to eq(file_path)
end
- it "should return a 400 bad request if no params given" do
+ it "returns a 400 bad request if no params given" do
delete api("/projects/#{project.id}/repository/files", user)
expect(response).to have_http_status(400)
end
- it "should return a 400 if fails to create file" do
+ it "returns a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:remove_file).and_return(false)
delete api("/projects/#{project.id}/repository/files", user), valid_params
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index a9f5aa924b7..f802fcd2d2e 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -20,7 +20,7 @@ describe API::API, api: true do
before { user3 }
context 'when authenticated' do
- it 'should fork if user has sufficient access to project' do
+ it 'forks if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
@@ -30,7 +30,7 @@ describe API::API, api: true do
expect(json_response['forked_from_project']['id']).to eq(project.id)
end
- it 'should fork if user is admin' do
+ it 'forks if user is admin' do
post api("/projects/fork/#{project.id}", admin)
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
@@ -40,20 +40,20 @@ describe API::API, api: true do
expect(json_response['forked_from_project']['id']).to eq(project.id)
end
- it 'should fail on missing project access for the project to fork' do
+ it 'fails on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
- it 'should fail if forked project exists in the user namespace' do
+ it 'fails if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'should fail if project to fork from does not exist' do
+ it 'fails if project to fork from does not exist' do
post api('/projects/fork/424242', user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
@@ -61,7 +61,7 @@ describe API::API, api: true do
end
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
post api("/projects/fork/#{project.id}")
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
diff --git a/spec/requests/api/group_members_spec.rb b/spec/requests/api/group_members_spec.rb
deleted file mode 100644
index 52f9e7d4681..00000000000
--- a/spec/requests/api/group_members_spec.rb
+++ /dev/null
@@ -1,199 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
-
- let(:owner) { create(:user) }
- let(:reporter) { create(:user) }
- let(:developer) { create(:user) }
- let(:master) { create(:user) }
- let(:guest) { create(:user) }
- let(:stranger) { create(:user) }
-
- let!(:group_with_members) do
- group = create(:group, :private)
- group.add_users([reporter.id], GroupMember::REPORTER)
- group.add_users([developer.id], GroupMember::DEVELOPER)
- group.add_users([master.id], GroupMember::MASTER)
- group.add_users([guest.id], GroupMember::GUEST)
- group
- end
-
- let!(:group_no_members) { create(:group) }
-
- before do
- group_with_members.add_owner owner
- group_no_members.add_owner owner
- end
-
- describe "GET /groups/:id/members" do
- context "when authenticated as user that is part or the group" do
- it "each user: should return an array of members groups of group3" do
- [owner, master, developer, reporter, guest].each do |user|
- get api("/groups/#{group_with_members.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(5)
- expect(json_response.find { |e| e['id'] == owner.id }['access_level']).to eq(GroupMember::OWNER)
- expect(json_response.find { |e| e['id'] == reporter.id }['access_level']).to eq(GroupMember::REPORTER)
- expect(json_response.find { |e| e['id'] == developer.id }['access_level']).to eq(GroupMember::DEVELOPER)
- expect(json_response.find { |e| e['id'] == master.id }['access_level']).to eq(GroupMember::MASTER)
- expect(json_response.find { |e| e['id'] == guest.id }['access_level']).to eq(GroupMember::GUEST)
- end
- end
-
- it 'users not part of the group should get access error' do
- get api("/groups/#{group_with_members.id}/members", stranger)
-
- expect(response).to have_http_status(404)
- end
- end
- end
-
- describe "POST /groups/:id/members" do
- context "when not a member of the group" do
- it "should not add guest as member of group_no_members when adding being done by person outside the group" do
- post api("/groups/#{group_no_members.id}/members", reporter), user_id: guest.id, access_level: GroupMember::MASTER
- expect(response).to have_http_status(403)
- end
- end
-
- context "when a member of the group" do
- it "should return ok and add new member" do
- new_user = create(:user)
-
- expect do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: new_user.id, access_level: GroupMember::MASTER
- end.to change { group_no_members.members.count }.by(1)
-
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq(new_user.name)
- expect(json_response['access_level']).to eq(GroupMember::MASTER)
- end
-
- it "should not allow guest to modify group members" do
- new_user = create(:user)
-
- expect do
- post api("/groups/#{group_with_members.id}/members", guest), user_id: new_user.id, access_level: GroupMember::MASTER
- end.not_to change { group_with_members.members.count }
-
- expect(response).to have_http_status(403)
- end
-
- it "should return error if member already exists" do
- post api("/groups/#{group_with_members.id}/members", owner), user_id: master.id, access_level: GroupMember::MASTER
- expect(response).to have_http_status(409)
- end
-
- it "should return a 400 error when user id is not given" do
- post api("/groups/#{group_no_members.id}/members", owner), access_level: GroupMember::MASTER
- expect(response).to have_http_status(400)
- end
-
- it "should return a 400 error when access level is not given" do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- post api("/groups/#{group_no_members.id}/members", owner), user_id: master.id, access_level: 1234
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'PUT /groups/:id/members/:user_id' do
- context 'when not a member of the group' do
- it 'should return a 409 error if the user is not a group member' do
- put(
- api("/groups/#{group_no_members.id}/members/#{developer.id}",
- owner), access_level: GroupMember::MASTER
- )
- expect(response).to have_http_status(404)
- end
- end
-
- context 'when a member of the group' do
- it 'should return ok and update member access level' do
- put(
- api("/groups/#{group_with_members.id}/members/#{reporter.id}",
- owner),
- access_level: GroupMember::MASTER
- )
-
- expect(response).to have_http_status(200)
-
- get api("/groups/#{group_with_members.id}/members", owner)
- json_reporter = json_response.find do |e|
- e['id'] == reporter.id
- end
-
- expect(json_reporter['access_level']).to eq(GroupMember::MASTER)
- end
-
- it 'should not allow guest to modify group members' do
- put(
- api("/groups/#{group_with_members.id}/members/#{developer.id}",
- guest),
- access_level: GroupMember::MASTER
- )
-
- expect(response).to have_http_status(403)
-
- get api("/groups/#{group_with_members.id}/members", owner)
- json_developer = json_response.find do |e|
- e['id'] == developer.id
- end
-
- expect(json_developer['access_level']).to eq(GroupMember::DEVELOPER)
- end
-
- it 'should return a 400 error when access level is not given' do
- put(
- api("/groups/#{group_with_members.id}/members/#{master.id}", owner)
- )
- expect(response).to have_http_status(400)
- end
-
- it 'should return a 422 error when access level is not known' do
- put(
- api("/groups/#{group_with_members.id}/members/#{master.id}", owner),
- access_level: 1234
- )
- expect(response).to have_http_status(422)
- end
- end
- end
-
- describe 'DELETE /groups/:id/members/:user_id' do
- context 'when not a member of the group' do
- it "should not delete guest's membership of group_with_members" do
- random_user = create(:user)
- delete api("/groups/#{group_with_members.id}/members/#{owner.id}", random_user)
-
- expect(response).to have_http_status(404)
- end
- end
-
- context "when a member of the group" do
- it "should delete guest's membership of group" do
- expect do
- delete api("/groups/#{group_with_members.id}/members/#{guest.id}", owner)
- end.to change { group_with_members.members.count }.by(-1)
-
- expect(response).to have_http_status(200)
- end
-
- it "should return a 404 error when user id is not known" do
- delete api("/groups/#{group_with_members.id}/members/1328", owner)
- expect(response).to have_http_status(404)
- end
-
- it "should not allow guest to modify group members" do
- delete api("/groups/#{group_with_members.id}/members/#{master.id}", guest)
- expect(response).to have_http_status(403)
- end
- end
- end
-end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c2c94040ece..4860b23c2ed 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -21,14 +21,14 @@ describe API::API, api: true do
describe "GET /groups" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/groups")
expect(response).to have_http_status(401)
end
end
context "when authenticated as user" do
- it "normal user: should return an array of groups of user1" do
+ it "normal user: returns an array of groups of user1" do
get api("/groups", user1)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -38,7 +38,7 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "admin: should return an array of all groups" do
+ it "admin: returns an array of all groups" do
get api("/groups", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -70,12 +70,12 @@ describe API::API, api: true do
expect(json_response['shared_projects'][0]['id']).to eq(project.id)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
- it "should not return a group not attached to user1" do
+ it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}", user1)
expect(response).to have_http_status(404)
@@ -83,31 +83,31 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "should return any existing group" do
+ it "returns any existing group" do
get api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group2.name)
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
end
context 'when using group path in URL' do
- it 'should return any existing group' do
+ it 'returns any existing group' do
get api("/groups/#{group1.path}", admin)
expect(response).to have_http_status(200)
expect(json_response['name']).to eq(group1.name)
end
- it 'should not return a non existing group' do
+ it 'does not return a non existing group' do
get api('/groups/unknown', admin)
expect(response).to have_http_status(404)
end
- it 'should not return a group not attached to user1' do
+ it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}", user1)
expect(response).to have_http_status(404)
@@ -161,7 +161,7 @@ describe API::API, api: true do
describe "GET /groups/:id/projects" do
context "when authenticated as user" do
- it "should return the group's projects" do
+ it "returns the group's projects" do
get api("/groups/#{group1.id}/projects", user1)
expect(response).to have_http_status(200)
@@ -170,12 +170,12 @@ describe API::API, api: true do
expect(project_names).to match_array([project1.name, project3.name])
end
- it "should not return a non existing group" do
+ it "does not return a non existing group" do
get api("/groups/1328/projects", user1)
expect(response).to have_http_status(404)
end
- it "should not return a group not attached to user1" do
+ it "does not return a group not attached to user1" do
get api("/groups/#{group2.id}/projects", user1)
expect(response).to have_http_status(404)
@@ -215,12 +215,12 @@ describe API::API, api: true do
expect(project_names).to match_array([project1.name, project3.name])
end
- it 'should not return a non existing group' do
+ it 'does not return a non existing group' do
get api('/groups/unknown/projects', admin)
expect(response).to have_http_status(404)
end
- it 'should not return a group not attached to user1' do
+ it 'does not return a group not attached to user1' do
get api("/groups/#{group2.path}/projects", user1)
expect(response).to have_http_status(404)
@@ -230,30 +230,30 @@ describe API::API, api: true do
describe "POST /groups" do
context "when authenticated as user without group permissions" do
- it "should not create group" do
+ it "does not create group" do
post api("/groups", user1), attributes_for(:group)
expect(response).to have_http_status(403)
end
end
context "when authenticated as user with group permissions" do
- it "should create group" do
+ it "creates group" do
post api("/groups", user3), attributes_for(:group)
expect(response).to have_http_status(201)
end
- it "should not create group, duplicate" do
+ it "does not create group, duplicate" do
post api("/groups", user3), { name: 'Duplicate Test', path: group2.path }
expect(response).to have_http_status(400)
expect(response.message).to eq("Bad Request")
end
- it "should return 400 bad request error if name not given" do
+ it "returns 400 bad request error if name not given" do
post api("/groups", user3), { path: group2.path }
expect(response).to have_http_status(400)
end
- it "should return 400 bad request error if path not given" do
+ it "returns 400 bad request error if path not given" do
post api("/groups", user3), { name: 'test' }
expect(response).to have_http_status(400)
end
@@ -262,24 +262,24 @@ describe API::API, api: true do
describe "DELETE /groups/:id" do
context "when authenticated as user" do
- it "should remove group" do
+ it "removes group" do
delete api("/groups/#{group1.id}", user1)
expect(response).to have_http_status(200)
end
- it "should not remove a group if not an owner" do
+ it "does not remove a group if not an owner" do
user4 = create(:user)
group1.add_master(user4)
delete api("/groups/#{group1.id}", user3)
expect(response).to have_http_status(403)
end
- it "should not remove a non existing group" do
+ it "does not remove a non existing group" do
delete api("/groups/1328", user1)
expect(response).to have_http_status(404)
end
- it "should not remove a group not attached to user1" do
+ it "does not remove a group not attached to user1" do
delete api("/groups/#{group2.id}", user1)
expect(response).to have_http_status(404)
@@ -287,12 +287,12 @@ describe API::API, api: true do
end
context "when authenticated as admin" do
- it "should remove any existing group" do
+ it "removes any existing group" do
delete api("/groups/#{group2.id}", admin)
expect(response).to have_http_status(200)
end
- it "should not remove a non existing group" do
+ it "does not remove a non existing group" do
delete api("/groups/1328", admin)
expect(response).to have_http_status(404)
end
@@ -308,14 +308,14 @@ describe API::API, api: true do
end
context "when authenticated as user" do
- it "should not transfer project to group" do
+ it "does not transfer project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", user2)
expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
- it "should transfer project to group" do
+ it "transfers project to group" do
post api("/groups/#{group1.id}/projects/#{project.id}", admin)
expect(response).to have_http_status(201)
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index e567d36afa8..5d06abcfeb3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -38,6 +38,68 @@ describe API::API, api: true do
end
end
+ describe 'GET /internal/two_factor_recovery_codes' do
+ it 'returns an error message when the key does not exist' do
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: 12345
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'returns an error message when the key is a deploy key' do
+ deploy_key = create(:deploy_key)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: deploy_key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes')
+ end
+
+ it 'returns an error message when the user does not exist' do
+ key_without_user = create(:key, user: nil)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key_without_user.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user for the given key')
+ expect(json_response['recovery_codes']).to be_nil
+ end
+
+ context 'when two-factor is enabled' do
+ it 'returns new recovery codes when the user exists' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
+ allow_any_instance_of(User)
+ .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_truthy
+ expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ end
+ end
+
+ context 'when two-factor is not enabled' do
+ it 'returns an error message' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['recovery_codes']).to be_nil
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -56,13 +118,21 @@ describe API::API, api: true do
context "git push with project.wiki" do
it 'responds with success' do
- project_wiki = create(:project, name: 'my.wiki', path: 'my.wiki')
- project_wiki.team << [user, :developer]
+ push(key, project.wiki)
+
+ expect(response).to have_http_status(200)
+ expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ end
+ end
- push(key, project_wiki)
+ context "git pull with project.wiki" do
+ it 'responds with success' do
+ pull(key, project.wiki)
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
+ expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
end
end
@@ -267,6 +337,24 @@ describe API::API, api: true do
end
end
+ describe 'GET /internal/merge_request_urls' do
+ let(:repo_name) { "#{project.namespace.name}/#{project.path}" }
+ let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") }
+
+ before do
+ project.team << [user, :developer]
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+ end
+
+ 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",
+ "new_merge_request" => true
+ }]
+ end
+ end
+
def pull(key, project, protocol = 'ssh')
post(
api("/internal/allowed"),
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 6adccb4ebae..b8038fc85a1 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -49,28 +49,29 @@ describe API::API, api: true do
describe "GET /issues" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/issues")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return an array of issues" do
+ it "returns an array of issues" do
get api("/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
end
- it "should add pagination headers and keep query params" do
+ 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 'should return an array of closed issues' do
+ it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -78,7 +79,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq(closed_issue.id)
end
- it 'should return an array of opened issues' do
+ it 'returns an array of opened issues' do
get api('/issues?state=opened', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -86,7 +87,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq(issue.id)
end
- it 'should return an array of all issues' do
+ it 'returns an array of all issues' do
get api('/issues?state=all', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -95,7 +96,7 @@ describe API::API, api: true do
expect(json_response.second['id']).to eq(closed_issue.id)
end
- it 'should return an array of labeled issues' do
+ it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -103,7 +104,7 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an array of labeled issues when at least one label matches' do
+ it 'returns an array of labeled issues when at least one label matches' do
get api("/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -111,14 +112,14 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an empty array if no issue matches labels' do
+ it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an array of labeled issues matching given state' do
+ it 'returns an array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -127,7 +128,7 @@ describe API::API, api: true do
expect(json_response.first['state']).to eq('opened')
end
- it 'should return an empty array if no issue matches labels and state filters' do
+ it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -282,7 +283,7 @@ describe API::API, api: true do
let(:base_url) { "/projects/#{project.id}" }
let(:title) { milestone.title }
- it 'should return project issues without confidential issues for non project members' do
+ 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)
expect(json_response).to be_an Array
@@ -290,7 +291,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project issues without confidential issues for project members with guest role' do
+ it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -298,7 +299,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for author' do
+ it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -306,7 +307,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for assignee' do
+ it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -314,7 +315,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project issues with confidential issues for project members' do
+ it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -322,7 +323,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return project confidential issues for admin' do
+ it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -330,7 +331,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(issue.title)
end
- it 'should return an array of labeled project issues' do
+ it 'returns an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -338,7 +339,7 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an array of labeled project issues when at least one label matches' do
+ it 'returns an array of labeled project issues when at least one label matches' do
get api("#{base_url}/issues?labels=#{label.title},foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -346,28 +347,28 @@ describe API::API, api: true do
expect(json_response.first['labels']).to eq([label.title])
end
- it 'should return an empty array if no project issue matches labels' do
+ it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an empty array if no issue matches milestone' do
+ it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an empty array if milestone does not exist' do
+ it 'returns an empty array if milestone does not exist' do
get api("#{base_url}/issues?milestone=foo", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- it 'should return an array of issues in given milestone' do
+ it 'returns an array of issues in given milestone' do
get api("#{base_url}/issues?milestone=#{title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -376,7 +377,7 @@ describe API::API, api: true do
expect(json_response.second['id']).to eq(closed_issue.id)
end
- it 'should return an array of issues matching state in milestone' do
+ it 'returns an array of issues matching state in milestone' do
get api("#{base_url}/issues?milestone=#{milestone.title}"\
'&state=closed', user)
expect(response).to have_http_status(200)
@@ -405,7 +406,7 @@ describe API::API, api: true do
expect(json_response['author']).to be_a Hash
end
- it "should return a project issue by id" do
+ it "returns a project issue by id" do
get api("/projects/#{project.id}/issues/#{issue.id}", user)
expect(response).to have_http_status(200)
@@ -413,7 +414,7 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
- it 'should return a project issue by iid' do
+ it 'returns a project issue by iid' do
get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
expect(response.status).to eq 200
expect(json_response.first['title']).to eq issue.title
@@ -421,44 +422,44 @@ describe API::API, api: true do
expect(json_response.first['iid']).to eq issue.iid
end
- it "should return 404 if issue id not found" do
+ it "returns 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response).to have_http_status(404)
end
context 'confidential issues' do
- it "should return 404 for non project members" do
+ it "returns 404 for non project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member)
expect(response).to have_http_status(404)
end
- it "should return 404 for project members with guest role" do
+ it "returns 404 for project members with guest role" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest)
expect(response).to have_http_status(404)
end
- it "should return confidential issue for project members" do
+ it "returns confidential issue for project members" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for author" do
+ it "returns confidential issue for author" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for assignee" do
+ it "returns confidential issue for assignee" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
expect(json_response['iid']).to eq(confidential_issue.iid)
end
- it "should return confidential issue for admin" do
+ it "returns confidential issue for admin" do
get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(confidential_issue.title)
@@ -468,7 +469,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/issues" do
- it "should create a new project issue" do
+ it "creates a new project issue" do
post api("/projects/#{project.id}/issues", user),
title: 'new issue', labels: 'label, label2'
expect(response).to have_http_status(201)
@@ -477,12 +478,12 @@ describe API::API, api: true do
expect(json_response['labels']).to eq(['label', 'label2'])
end
- it "should return a 400 bad request if title not given" do
+ it "returns a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
expect(response).to have_http_status(400)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
post api("/projects/#{project.id}/issues", user),
title: 'new issue',
labels: 'label, label?, label&foo, ?, &'
@@ -494,7 +495,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include '&'
end
- it 'should return 400 if title is too long' do
+ it 'returns 400 if title is too long' do
post api("/projects/#{project.id}/issues", user),
title: 'g' * 256
expect(response).to have_http_status(400)
@@ -503,6 +504,20 @@ describe API::API, api: true do
])
end
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', due_date: due_date
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
@@ -517,10 +532,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/issues with spam filtering' do
before do
- Grape::Endpoint.before_each do |endpoint|
- allow(endpoint).to receive(:check_for_spam?).and_return(true)
- allow(endpoint).to receive(:is_spam?).and_return(true)
- end
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
end
let(:params) do
@@ -531,7 +544,7 @@ describe API::API, api: true do
}
end
- it "should not create a new project issue" do
+ it "does not create a new project issue" do
expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count)
expect(response).to have_http_status(400)
expect(json_response['message']).to eq({ "error" => "Spam detected" })
@@ -542,12 +555,11 @@ describe API::API, api: true do
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
- expect(spam_logs[0].project_id).to eq(project.id)
end
end
describe "PUT /projects/:id/issues/:issue_id to update only title" do
- it "should update a project issue" do
+ it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
@@ -555,13 +567,13 @@ describe API::API, api: true do
expect(json_response['title']).to eq('updated title')
end
- it "should return 404 error if issue id not found" do
+ it "returns 404 error if issue id not found" do
put api("/projects/#{project.id}/issues/44444", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title',
labels: 'label, label?, label&foo, ?, &'
@@ -575,33 +587,33 @@ describe API::API, api: true do
end
context 'confidential issues' do
- it "should return 403 for non project members" do
+ it "returns 403 for non project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member),
title: 'updated title'
expect(response).to have_http_status(403)
end
- it "should return 403 for project members with guest role" do
+ it "returns 403 for project members with guest role" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest),
title: 'updated title'
expect(response).to have_http_status(403)
end
- it "should update a confidential issue for project members" do
+ it "updates a confidential issue for project members" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it "should update a confidential issue for author" do
+ it "updates a confidential issue for author" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it "should update a confidential issue for admin" do
+ it "updates a confidential issue for admin" do
put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin),
title: 'updated title'
expect(response).to have_http_status(200)
@@ -614,21 +626,21 @@ describe API::API, api: true do
let!(:label) { create(:label, title: 'dummy', project: project) }
let!(:label_link) { create(:label_link, label: label, target: issue) }
- it 'should not update labels if not present' do
+ it 'does not update labels if not present' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([label.title])
end
- it 'should remove all labels' do
+ it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: ''
expect(response).to have_http_status(200)
expect(json_response['labels']).to eq([])
end
- it 'should update labels' do
+ it 'updates labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'foo,bar'
expect(response).to have_http_status(200)
@@ -636,7 +648,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include 'bar'
end
- it 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&'
expect(response.status).to eq(200)
@@ -650,7 +662,7 @@ describe API::API, api: true do
expect(json_response['labels']).to include '&'
end
- it 'should return 400 if title is too long' do
+ it 'returns 400 if title is too long' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
title: 'g' * 256
expect(response).to have_http_status(400)
@@ -661,7 +673,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/issues/:issue_id to update state and label" do
- it "should update a project issue" do
+ it "updates a project issue" do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: 'label2', state_event: "close"
expect(response).to have_http_status(200)
@@ -683,6 +695,17 @@ describe API::API, api: true do
end
end
+ describe 'PUT /projects/:id/issues/:issue_id to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date
+
+ expect(response).to have_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
describe "DELETE /projects/:id/issues/:issue_id" do
it "rejects a non member from deleting an issue" do
delete api("/projects/#{project.id}/issues/#{issue.id}", non_member)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 1861882d59e..893ed5c2b10 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -12,20 +12,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/keys/#{key.id}")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing key' do
+ it 'returns 404 for non-existing key' do
get api('/keys/999999', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it 'should return single ssh key with user information' do
+ it 'returns single ssh key with user information' do
user.keys << key
user.save
get api("/keys/#{key.id}", admin)
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index 63636b4a1b6..83789223019 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/labels' do
- it 'should return project labels' do
+ it 'returns project labels' do
get api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -22,7 +22,7 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/labels' do
- it 'should return created label when all params' do
+ it 'returns created label when all params' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAABB',
@@ -33,7 +33,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return created label when only required params' do
+ it 'returns created label when only required params' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo & Bar',
color: '#FFAABB'
@@ -43,17 +43,17 @@ describe API::API, api: true do
expect(json_response['description']).to be_nil
end
- it 'should return a 400 bad request if name not given' do
+ it 'returns a 400 bad request if name not given' do
post api("/projects/#{project.id}/labels", user), color: '#FFAABB'
expect(response).to have_http_status(400)
end
- it 'should return a 400 bad request if color not given' do
+ it 'returns a 400 bad request if color not given' do
post api("/projects/#{project.id}/labels", user), name: 'Foobar'
expect(response).to have_http_status(400)
end
- it 'should return 400 for invalid color' do
+ it 'returns 400 for invalid color' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAA'
@@ -61,7 +61,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for too long color code' do
+ it 'returns 400 for too long color code' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
@@ -69,7 +69,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for invalid name' do
+ it 'returns 400 for invalid name' do
post api("/projects/#{project.id}/labels", user),
name: ',',
color: '#FFAABB'
@@ -77,7 +77,7 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 409 if label already exists' do
+ it 'returns 409 if label already exists' do
post api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFAABB'
@@ -87,25 +87,25 @@ describe API::API, api: true do
end
describe 'DELETE /projects/:id/labels' do
- it 'should return 200 for existing label' do
+ it 'returns 200 for existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label1'
expect(response).to have_http_status(200)
end
- it 'should return 404 for non existing label' do
+ it 'returns 404 for non existing label' do
delete api("/projects/#{project.id}/labels", user), name: 'label2'
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Label Not Found')
end
- it 'should return 400 for wrong parameters' do
+ it 'returns 400 for wrong parameters' do
delete api("/projects/#{project.id}/labels", user)
expect(response).to have_http_status(400)
end
end
describe 'PUT /projects/:id/labels' do
- it 'should return 200 if name and colors and description are changed' do
+ it 'returns 200 if name and colors and description are changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label',
@@ -117,7 +117,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return 200 if name is changed' do
+ it 'returns 200 if name is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: 'New Label'
@@ -126,7 +126,7 @@ describe API::API, api: true do
expect(json_response['color']).to eq(label1.color)
end
- it 'should return 200 if colors is changed' do
+ it 'returns 200 if colors is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FFFFFF'
@@ -135,7 +135,7 @@ describe API::API, api: true do
expect(json_response['color']).to eq('#FFFFFF')
end
- it 'should return 200 if description is changed' do
+ it 'returns 200 if description is changed' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
description: 'test'
@@ -144,27 +144,27 @@ describe API::API, api: true do
expect(json_response['description']).to eq('test')
end
- it 'should return 404 if label does not exist' do
+ it 'returns 404 if label does not exist' do
put api("/projects/#{project.id}/labels", user),
name: 'label2',
new_name: 'label3'
expect(response).to have_http_status(404)
end
- it 'should return 400 if no label name given' do
+ it 'returns 400 if no label name given' do
put api("/projects/#{project.id}/labels", user), new_name: 'label2'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('400 (Bad request) "name" not given')
end
- it 'should return 400 if no new parameters given' do
+ it 'returns 400 if no new parameters given' do
put api("/projects/#{project.id}/labels", user), name: 'label1'
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('Required parameters '\
'"new_name" or "color" missing')
end
- it 'should return 400 for invalid name' do
+ it 'returns 400 for invalid name' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
new_name: ',',
@@ -173,7 +173,7 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 400 when color code is too short' do
+ it 'returns 400 when color code is too short' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
@@ -181,7 +181,7 @@ describe API::API, api: true do
expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
- it 'should return 400 for too long color code' do
+ it 'returns 400 for too long color code' do
post api("/projects/#{project.id}/labels", user),
name: 'Foo',
color: '#FFAAFFFF'
@@ -192,7 +192,7 @@ describe API::API, api: true do
describe "POST /projects/:id/labels/:label_id/subscription" do
context "when label_id is a label title" do
- it "should subscribe to the label" do
+ it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(201)
@@ -202,7 +202,7 @@ describe API::API, api: true do
end
context "when label_id is a label ID" do
- it "should subscribe to the label" do
+ it "subscribes to the label" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(201)
@@ -214,7 +214,7 @@ describe API::API, api: true do
context "when user is already subscribed to label" do
before { label1.subscribe(user) }
- it "should return 304" do
+ it "returns 304" do
post api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
@@ -222,7 +222,7 @@ describe API::API, api: true do
end
context "when label ID is not found" do
- it "should a return 404 error" do
+ it "returns 404 error" do
post api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
@@ -234,7 +234,7 @@ describe API::API, api: true do
before { label1.subscribe(user) }
context "when label_id is a label title" do
- it "should unsubscribe from the label" do
+ it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.title}/subscription", user)
expect(response).to have_http_status(200)
@@ -244,7 +244,7 @@ describe API::API, api: true do
end
context "when label_id is a label ID" do
- it "should unsubscribe from the label" do
+ it "unsubscribes from the label" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(200)
@@ -256,7 +256,7 @@ describe API::API, api: true do
context "when user is already unsubscribed from label" do
before { label1.unsubscribe(user) }
- it "should return 304" do
+ it "returns 304" do
delete api("/projects/#{project.id}/labels/#{label1.id}/subscription", user)
expect(response).to have_http_status(304)
@@ -264,7 +264,7 @@ describe API::API, api: true do
end
context "when label ID is not found" do
- it "should a return 404 error" do
+ it "returns 404 error" do
delete api("/projects/#{project.id}/labels/1234/subscription", user)
expect(response).to have_http_status(404)
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
new file mode 100644
index 00000000000..1e365bf353a
--- /dev/null
+++ b/spec/requests/api/members_spec.rb
@@ -0,0 +1,314 @@
+require 'spec_helper'
+
+describe API::Members, api: true do
+ include ApiHelpers
+
+ let(:master) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+
+ let(:project) do
+ 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
+ end
+
+ let!(:group) do
+ group = create(:group, :public)
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group
+ end
+
+ shared_examples 'GET /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ it 'finds members with query string' do
+ get api("/#{source_type.pluralize}/#{source.id}/members", developer), query: master.username
+
+ expect(response).to have_http_status(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first['username']).to eq(master.username)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ get api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(200)
+ # User attributes
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['name']).to eq(developer.name)
+ expect(json_response['username']).to eq(developer.username)
+ expect(json_response['state']).to eq(developer.state)
+ expect(json_response['avatar_url']).to eq(developer.avatar_url)
+ expect(json_response['web_url']).to eq(Gitlab::Routing.url_helpers.user_url(developer))
+
+ # Member attributes
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/members' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ post api("/#{source_type.pluralize}/#{source.id}/members", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and new member is already a requester' do
+ it 'transforms the requester into a proper member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: access_requester.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(source.requesters.count).to eq(0)
+ expect(json_response['id']).to eq(access_requester.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ end
+ end
+
+ it 'creates a new member' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(201)
+ end.to change { source.members.count }.by(1)
+ expect(json_response['id']).to eq(stranger.id)
+ expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 201 : 409} if member already exists" do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: master.id, access_level: Member::MASTER
+
+ expect(response).to have_http_status(source_type == 'project' ? 201 : 409)
+ end
+
+ it 'returns 400 when user_id is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access_level is not valid' do
+ post api("/#{source_type.pluralize}/#{source.id}/members", master),
+ user_id: stranger.id, access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: Member::MASTER, expires_at: '2016-08-05'
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(developer.id)
+ expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
+ end
+ end
+
+ it 'returns 409 if member does not exist' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/123", master),
+ access_level: Member::MASTER
+
+ expect(response).to have_http_status(404)
+ end
+
+ it 'returns 400 when access_level is not given' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(400)
+ end
+
+ it 'returns 422 when access level is not valid' do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
+ access_level: 1234
+
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/members/:user_id' do |source_type|
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a member and deleting themself' do
+ it 'deletes the member' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", developer)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ context 'and member is a requester' do
+ it "returns #{source_type == 'project' ? 200 : 404}" do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{access_requester.id}", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
+ it 'deletes the member' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master)
+
+ expect(response).to have_http_status(200)
+ end.to change { source.members.count }.by(-1)
+ end
+ end
+
+ it "returns #{source_type == 'project' ? 200 : 404} if member does not exist" do
+ delete api("/#{source_type.pluralize}/#{source.id}/members/123", master)
+
+ expect(response).to have_http_status(source_type == 'project' ? 200 : 404)
+ end
+ end
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'GET /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'POST /:sources/:id/members', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'PUT /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'project' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'DELETE /:sources/:id/members/:user_id', 'group' do
+ let(:source) { group }
+ end
+end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
new file mode 100644
index 00000000000..8f1e5ac9891
--- /dev/null
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::API, 'MergeRequestDiffs', api: true do
+ include ApiHelpers
+
+ let!(:user) { create(:user) }
+ let!(:merge_request) { create(:merge_request, importing: true) }
+ let!(:project) { merge_request.target_project }
+
+ before do
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) }
+ it { expect(json_response.first['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 4a1b5600bdf..baff872e28e 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -20,22 +20,23 @@ describe API::API, api: true do
describe "GET /projects/:id/merge_requests" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/projects/#{project.id}/merge_requests")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return an array of all merge_requests" do
+ it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
end
- it "should return an array of all merge_requests" do
+ it "returns an array of all merge_requests" do
get api("/projects/#{project.id}/merge_requests?state", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -43,7 +44,7 @@ describe API::API, api: true do
expect(json_response.last['title']).to eq(merge_request.title)
end
- it "should return an array of open merge_requests" do
+ it "returns an array of open merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -51,7 +52,7 @@ describe API::API, api: true do
expect(json_response.last['title']).to eq(merge_request.title)
end
- it "should return an array of closed merge_requests" do
+ it "returns an array of closed merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=closed", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -59,7 +60,7 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
- it "should return an array of merged merge_requests" do
+ it "returns an array of merged merge_requests" do
get api("/projects/#{project.id}/merge_requests?state=merged", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -73,7 +74,7 @@ describe API::API, api: true do
@mr_earlier = mr_with_earlier_created_and_updated_at_time
end
- it "should return an array of merge_requests in ascending order" do
+ it "returns an array of merge_requests in ascending order" do
get api("/projects/#{project.id}/merge_requests?sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -82,7 +83,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort)
end
- it "should return an array of merge_requests in descending order" do
+ it "returns an array of merge_requests in descending order" do
get api("/projects/#{project.id}/merge_requests?sort=desc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -91,7 +92,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort.reverse)
end
- it "should return an array of merge_requests ordered by updated_at" do
+ it "returns an array of merge_requests ordered by updated_at" do
get api("/projects/#{project.id}/merge_requests?order_by=updated_at", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -100,7 +101,7 @@ describe API::API, api: true do
expect(response_dates).to eq(response_dates.sort.reverse)
end
- it "should return an array of merge_requests ordered by created_at" do
+ it "returns an array of merge_requests ordered by created_at" do
get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -138,18 +139,22 @@ describe API::API, api: true do
expect(json_response['work_in_progress']).to be_falsy
expect(json_response['merge_when_build_succeeds']).to be_falsy
expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
end
- it "should return merge_request" do
+ it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(merge_request.title)
expect(json_response['iid']).to eq(merge_request.iid)
expect(json_response['work_in_progress']).to eq(false)
expect(json_response['merge_status']).to eq('can_be_merged')
+ expect(json_response['should_close_merge_request']).to be_falsy
+ expect(json_response['force_close_merge_request']).to be_falsy
end
- it 'should return merge_request by iid' do
+ it 'returns merge_request by iid' do
url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
get api(url, user)
expect(response.status).to eq 200
@@ -157,7 +162,7 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq merge_request.id
end
- it "should return a 404 error if merge_request_id not found" do
+ 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)
end
@@ -165,7 +170,7 @@ describe API::API, api: true do
context 'Work in Progress' do
let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
- it "should return merge_request" do
+ it "returns merge_request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user)
expect(response).to have_http_status(200)
expect(json_response['work_in_progress']).to eq(true)
@@ -191,7 +196,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do
- it 'should return the change information of the merge_request' do
+ it 'returns the change information of the merge_request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user)
expect(response.status).to eq 200
expect(json_response['changes'].size).to eq(merge_request.diffs.size)
@@ -205,7 +210,7 @@ describe API::API, api: true do
describe "POST /projects/:id/merge_requests" do
context 'between branches projects' do
- it "should return merge_request" do
+ it "returns merge_request" do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'feature_conflict',
@@ -219,31 +224,31 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "should return 422 when source_branch equals target_branch" do
+ it "returns 422 when source_branch equals target_branch" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "master", target_branch: "master", author: user
expect(response).to have_http_status(422)
end
- it "should return 400 when source_branch is missing" do
+ it "returns 400 when source_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", target_branch: "master", author: user
expect(response).to have_http_status(400)
end
- it "should return 400 when target_branch is missing" do
+ it "returns 400 when target_branch is missing" do
post api("/projects/#{project.id}/merge_requests", user),
title: "Test merge_request", source_branch: "markdown", author: user
expect(response).to have_http_status(400)
end
- it "should return 400 when title is missing" do
+ it "returns 400 when title is missing" do
post api("/projects/#{project.id}/merge_requests", user),
target_branch: 'master', source_branch: 'markdown'
expect(response).to have_http_status(400)
end
- it 'should allow special label names' do
+ it 'allows special label names' do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
source_branch: 'markdown',
@@ -268,7 +273,7 @@ describe API::API, api: true do
@mr = MergeRequest.all.last
end
- it 'should return 409 when MR already exists for source/target' do
+ it 'returns 409 when MR already exists for source/target' do
expect do
post api("/projects/#{project.id}/merge_requests", user),
title: 'New test merge_request',
@@ -290,7 +295,7 @@ describe API::API, api: true do
fork_project.team << [user2, :reporters]
end
- it "should return merge_request" do
+ it "returns merge_request" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master",
author: user2, target_project_id: project.id, description: 'Test description for Test merge_request'
@@ -299,7 +304,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq('Test description for Test merge_request')
end
- it "should not return 422 when source_branch equals target_branch" do
+ it "does not return 422 when source_branch equals target_branch" do
expect(project.id).not_to eq(fork_project.id)
expect(fork_project.forked?).to be_truthy
expect(fork_project.forked_from_project).to eq(project)
@@ -309,26 +314,26 @@ describe API::API, api: true do
expect(json_response['title']).to eq('Test merge_request')
end
- it "should return 400 when source_branch is missing" do
+ it "returns 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
- it "should return 400 when target_branch is missing" do
+ it "returns 400 when target_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
- it "should return 400 when title is missing" do
+ it "returns 400 when title is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id
expect(response).to have_http_status(400)
end
context 'when target_branch is specified' do
- it 'should return 422 if not a forked project' do
+ it 'returns 422 if not a forked project' do
post api("/projects/#{project.id}/merge_requests", user),
title: 'Test merge_request',
target_branch: 'master',
@@ -338,7 +343,7 @@ describe API::API, api: true do
expect(response).to have_http_status(422)
end
- it 'should return 422 if targeting a different fork' do
+ it 'returns 422 if targeting a different fork' do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request',
target_branch: 'master',
@@ -349,7 +354,7 @@ describe API::API, api: true do
end
end
- it "should return 201 when target_branch is specified and for the same project" do
+ it "returns 201 when target_branch is specified and for the same project" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id
expect(response).to have_http_status(201)
@@ -381,7 +386,7 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do
- it "should return merge_request" 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')
@@ -391,13 +396,13 @@ describe API::API, api: true do
describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do
let(:pipeline) { create(:ci_pipeline_without_jobs) }
- it "should return merge_request in case of success" do
+ it "returns merge_request in case of success" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(200)
end
- it "should return 406 if branch can't be merged" do
+ it "returns 406 if branch can't be merged" do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
@@ -407,14 +412,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Branch cannot be merged')
end
- it "should return 405 if merge_request is not open" do
+ it "returns 405 if merge_request is not open" do
merge_request.close
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
expect(json_response['message']).to eq('405 Method Not Allowed')
end
- it "should return 405 if merge_request is a work in progress" do
+ it "returns 405 if merge_request is a work in progress" do
merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user)
expect(response).to have_http_status(405)
@@ -430,7 +435,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
- it "should return 401 if user has no permissions to merge" do
+ it "returns 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2)
@@ -482,19 +487,19 @@ describe API::API, api: true do
expect(json_response['milestone']['id']).to eq(milestone.id)
end
- it "should return 400 when source_branch is specified" do
+ 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 "should return merge_request with renamed target_branch" do
+ 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 'should allow special label names' do
+ it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}",
user),
title: 'new issue',
@@ -509,7 +514,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do
- it "should return comment" do
+ it "returns comment" do
original_count = merge_request.notes.size
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment"
@@ -520,12 +525,12 @@ describe API::API, api: true do
expect(merge_request.notes.size).to eq(original_count + 1)
end
- it "should return 400 if note is missing" do
+ it "returns 400 if note is missing" do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(400)
end
- it "should return 404 if note is attached to non existent merge request" 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'
expect(response).to have_http_status(404)
@@ -533,7 +538,7 @@ describe API::API, api: true do
end
describe "GET :id/merge_requests/:merge_request_id/comments" do
- it "should return merge_request comments ordered by created_at" do
+ it "returns merge_request comments ordered by created_at" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -543,7 +548,7 @@ describe API::API, api: true do
expect(json_response.last['note']).to eq("another comment on a MR")
end
- it "should return a 404 error if merge_request_id not found" do
+ it "returns a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_requests/999/comments", user)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 0f4e38b2475..d6a0c656e74 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -10,14 +10,14 @@ describe API::API, api: true do
before { project.team << [user, :developer] }
describe 'GET /projects/:id/milestones' do
- it 'should return project milestones' do
+ it 'returns project milestones' do
get api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(milestone.title)
end
- it 'should return a 401 error if user not authenticated' do
+ it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones")
expect(response).to have_http_status(401)
end
@@ -42,14 +42,14 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'should return a project milestone by id' do
+ it 'returns a project milestone by id' do
get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
expect(json_response['iid']).to eq(milestone.iid)
end
- it 'should return a project milestone by iid' do
+ it 'returns a project milestone by iid' do
get api("/projects/#{project.id}/milestones?iid=#{closed_milestone.iid}", user)
expect(response.status).to eq 200
@@ -58,26 +58,26 @@ describe API::API, api: true do
expect(json_response.first['id']).to eq closed_milestone.id
end
- it 'should return 401 error if user not authenticated' do
+ it 'returns 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
expect(response).to have_http_status(401)
end
- it 'should return a 404 error if milestone id not found' do
+ it 'returns a 404 error if milestone id not found' do
get api("/projects/#{project.id}/milestones/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/milestones' do
- it 'should create a new project milestone' do
+ it 'creates a new project milestone' do
post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
- it 'should create a new project milestone with description and due date' do
+ it 'creates a new project milestone with description and due date' do
post api("/projects/#{project.id}/milestones", user),
title: 'new milestone', description: 'release', due_date: '2013-03-02'
expect(response).to have_http_status(201)
@@ -85,21 +85,21 @@ describe API::API, api: true do
expect(json_response['due_date']).to eq('2013-03-02')
end
- it 'should return a 400 error if title is missing' do
+ it 'returns a 400 error if title is missing' do
post api("/projects/#{project.id}/milestones", user)
expect(response).to have_http_status(400)
end
end
describe 'PUT /projects/:id/milestones/:milestone_id' do
- it 'should update a project milestone' do
+ it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
title: 'updated title'
expect(response).to have_http_status(200)
expect(json_response['title']).to eq('updated title')
end
- it 'should return a 404 error if milestone id not found' do
+ it 'returns a 404 error if milestone id not found' do
put api("/projects/#{project.id}/milestones/1234", user),
title: 'updated title'
expect(response).to have_http_status(404)
@@ -107,7 +107,7 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
- it 'should update a project milestone' do
+ it 'updates a project milestone' do
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
state_event: 'close'
expect(response).to have_http_status(200)
@@ -117,7 +117,7 @@ describe API::API, api: true do
end
describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
- it 'should create an activity event when an milestone is closed' do
+ it 'creates an activity event when an milestone is closed' do
expect(Event).to receive(:create)
put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
@@ -129,14 +129,14 @@ describe API::API, api: true do
before do
milestone.issues << create(:issue, project: project)
end
- it 'should return project issues for a particular milestone' do
+ it 'returns project issues for a particular milestone' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
- it 'should return a 401 error if user not authenticated' do
+ it 'returns a 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
expect(response).to have_http_status(401)
end
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 237b4b17eb5..5347cf4f7bc 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -9,14 +9,14 @@ describe API::API, api: true do
describe "GET /namespaces" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/namespaces")
expect(response).to have_http_status(401)
end
end
context "when authenticated as admin" do
- it "admin: should return an array of all namespaces" do
+ it "admin: returns an array of all namespaces" do
get api("/namespaces", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -24,7 +24,7 @@ describe API::API, api: true do
expect(json_response.length).to eq(Namespace.count)
end
- it "admin: should return an array of matched namespaces" do
+ it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{group1.name}", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -34,7 +34,7 @@ describe API::API, api: true do
end
context "when authenticated as a regular user" do
- it "user: should return an array of namespaces" do
+ it "user: returns an array of namespaces" do
get api("/namespaces", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -42,7 +42,7 @@ describe API::API, api: true do
expect(json_response.length).to eq(1)
end
- it "admin: should return an array of matched namespaces" do
+ it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 65c53211dd3..223444ea39f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -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
@@ -37,7 +37,7 @@ describe API::API, api: true do
end
context "when noteable is an Issue" do
- it "should return an array of issue notes" do
+ it "returns an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
expect(response).to have_http_status(200)
@@ -45,14 +45,14 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(issue_note.note)
end
- it "should return a 404 error when issue id not found" do
+ it "returns a 404 error when issue id not found" do
get api("/projects/#{project.id}/issues/12345/notes", user)
expect(response).to have_http_status(404)
end
context "and current user cannot view the notes" do
- it "should return an empty array" do
+ it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
expect(response).to have_http_status(200)
@@ -71,7 +71,7 @@ describe API::API, api: true do
end
context "and current user can view the note" do
- it "should return an empty array" do
+ it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
expect(response).to have_http_status(200)
@@ -83,7 +83,7 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should return an array of snippet notes" do
+ it "returns an array of snippet notes" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
expect(response).to have_http_status(200)
@@ -91,7 +91,7 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(snippet_note.note)
end
- it "should return a 404 error when snippet id not found" do
+ it "returns a 404 error when snippet id not found" do
get api("/projects/#{project.id}/snippets/42/notes", user)
expect(response).to have_http_status(404)
@@ -105,7 +105,7 @@ describe API::API, api: true do
end
context "when noteable is a Merge Request" do
- it "should return an array of merge_requests notes" do
+ it "returns an array of merge_requests notes" do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
expect(response).to have_http_status(200)
@@ -113,7 +113,7 @@ describe API::API, api: true do
expect(json_response.first['body']).to eq(merge_request_note.note)
end
- it "should return a 404 error if merge request id not found" do
+ it "returns a 404 error if merge request id not found" do
get api("/projects/#{project.id}/merge_requests/4444/notes", user)
expect(response).to have_http_status(404)
@@ -129,21 +129,21 @@ describe API::API, api: true do
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
context "when noteable is an Issue" do
- it "should return an issue note by id" do
+ it "returns an issue note by id" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
- it "should return a 404 error if issue note not found" do
+ it "returns a 404 error if issue note not found" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
expect(response).to have_http_status(404)
end
context "and current user cannot view the note" do
- it "should return a 404 error" do
+ it "returns a 404 error" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
expect(response).to have_http_status(404)
@@ -160,7 +160,7 @@ describe API::API, api: true do
end
context "and current user can view the note" do
- it "should return an issue note by id" do
+ it "returns an issue note by id" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
expect(response).to have_http_status(200)
@@ -171,14 +171,14 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should return a snippet note by id" do
+ it "returns a snippet note by id" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(snippet_note.note)
end
- it "should return a 404 error if snippet note not found" do
+ it "returns a 404 error if snippet note not found" do
get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/12345", user)
expect(response).to have_http_status(404)
@@ -188,7 +188,7 @@ describe API::API, api: true do
describe "POST /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
- it "should create a new issue note" do
+ it "creates a new issue note" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
expect(response).to have_http_status(201)
@@ -196,13 +196,13 @@ describe API::API, api: true do
expect(json_response['author']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if body not given" do
+ it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if user not authenticated" do
+ it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
expect(response).to have_http_status(401)
@@ -223,7 +223,7 @@ describe API::API, api: true do
end
context "when noteable is a Snippet" do
- it "should create a new snippet note" do
+ it "creates a new snippet note" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!'
expect(response).to have_http_status(201)
@@ -231,13 +231,13 @@ describe API::API, api: true do
expect(json_response['author']['username']).to eq(user.username)
end
- it "should return a 400 bad request error if body not given" do
+ it "returns a 400 bad request error if body not given" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user)
expect(response).to have_http_status(400)
end
- it "should return a 401 unauthorized error if user not authenticated" do
+ it "returns a 401 unauthorized error if user not authenticated" do
post api("/projects/#{project.id}/snippets/#{snippet.id}/notes"), body: 'hi!'
expect(response).to have_http_status(401)
@@ -267,7 +267,7 @@ describe API::API, api: true do
end
describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do
- it "should create an activity event when an issue note is created" do
+ it "creates an activity event when an issue note is created" do
expect(Event).to receive(:create)
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
@@ -276,7 +276,7 @@ describe API::API, api: true do
describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
context 'when noteable is an Issue' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
@@ -284,14 +284,14 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
body: 'Hello!'
expect(response).to have_http_status(404)
end
- it 'should return a 400 bad request error if body not given' do
+ it 'returns a 400 bad request error if body not given' do
put api("/projects/#{project.id}/issues/#{issue.id}/"\
"notes/#{issue_note.id}", user)
@@ -300,7 +300,7 @@ describe API::API, api: true do
end
context 'when noteable is a Snippet' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/#{snippet_note.id}", user), body: 'Hello!'
@@ -308,7 +308,7 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/snippets/#{snippet.id}/"\
"notes/12345", user), body: "Hello!"
@@ -317,7 +317,7 @@ describe API::API, api: true do
end
context 'when noteable is a Merge Request' do
- it 'should return modified note' do
+ it 'returns modified note' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
@@ -325,7 +325,7 @@ describe API::API, api: true do
expect(json_response['body']).to eq('Hello!')
end
- it 'should return a 404 error when note id not found' do
+ it 'returns a 404 error when note id not found' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
"notes/12345", user), body: "Hello!"
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
new file mode 100644
index 00000000000..7e2cc50e591
--- /dev/null
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ context 'Resource Owner Password Credentials' do
+ def request_oauth_token(user)
+ post '/oauth/token', username: user.username, password: user.password, grant_type: 'password'
+ end
+
+ context 'when user has 2FA enabled' do
+ it 'does not create an access token' do
+ user = create(:user, :two_factor)
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ expect(json_response['error']).to eq('invalid_grant')
+ end
+ end
+
+ context 'when user does not have 2FA enabled' do
+ it 'creates an access token' do
+ user = create(:user)
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['access_token']).not_to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
new file mode 100644
index 00000000000..7011bdc9ec0
--- /dev/null
+++ b/spec/requests/api/pipelines_spec.rb
@@ -0,0 +1,133 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, creator_id: user.id) }
+
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ before { project.team << [user, :master] }
+
+ describe 'GET /projects/:id/pipelines ' do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/pipelines", user) }
+ end
+
+ context 'authorized user' do
+ it 'returns project pipelines' do
+ get api("/projects/#{project.id}/pipelines", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['sha']).to match /\A\h{40}\z/
+ expect(json_response.first['id']).to eq pipeline.id
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'does not return project pipelines' do
+ get api("/projects/#{project.id}/pipelines", non_member)
+
+ 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
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['sha']).to match /\A\h{40}\z/
+ end
+
+ it 'returns 404 when it does not exist' do
+ get api("/projects/#{project.id}/pipelines/123456", user)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Not found'
+ expect(json_response['id']).to be nil
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/retry' do
+ context 'authorized user' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :failed, pipeline: pipeline) }
+
+ it 'retries failed builds' do
+ expect do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", user)
+ end.to change { pipeline.builds.count }.from(1).to(2)
+
+ expect(response).to have_http_status(201)
+ expect(build.reload.retried?).to be true
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return a project pipeline' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ expect(json_response['id']).to be nil
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/pipelines/:pipeline_id/cancel' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: project.commit.id,
+ ref: project.default_branch)
+ end
+
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ context 'authorized user' do
+ it 'retries failed builds' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('canceled')
+ end
+ end
+
+ context 'user without proper access rights' do
+ let!(:reporter) { create(:user) }
+
+ before { project.team << [reporter, :reporter] }
+
+ it 'rejects the action' do
+ post api("/projects/#{project.id}/pipelines/#{pipeline.id}/cancel", reporter)
+
+ expect(response).to have_http_status(403)
+ expect(pipeline.reload.status).to eq('pending')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index fd1fffa6223..765dc8a8f66 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -7,9 +7,9 @@ describe API::API, 'ProjectHooks', api: true do
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:hook) do
create(:project_hook,
- project: project, url: "http://example.com",
- push_events: true, merge_requests_events: true, tag_push_events: true,
- issues_events: true, note_events: true, build_events: true,
+ :all_events_enabled,
+ project: project,
+ url: 'http://example.com',
enable_ssl_verification: true)
end
@@ -20,7 +20,7 @@ describe API::API, 'ProjectHooks', api: true do
describe "GET /projects/:id/hooks" do
context "authorized user" do
- it "should return project hooks" do
+ it "returns project hooks" do
get api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(200)
@@ -33,12 +33,14 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
+ expect(json_response.first['pipeline_events']).to eq(true)
+ expect(json_response.first['wiki_page_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
context "unauthorized user" do
- it "should not access project hooks" do
+ it "does not access project hooks" do
get api("/projects/#{project.id}/hooks", user3)
expect(response).to have_http_status(403)
end
@@ -47,7 +49,7 @@ describe API::API, 'ProjectHooks', api: true do
describe "GET /projects/:id/hooks/:hook_id" do
context "authorized user" do
- it "should return a project hook" do
+ it "returns a project hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(200)
expect(json_response['url']).to eq(hook.url)
@@ -56,30 +58,33 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
- it "should return a 404 error if hook id is not available" do
+ it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
expect(response).to have_http_status(404)
end
end
context "unauthorized user" do
- it "should not access an existing hook" do
+ it "does not access an existing hook" do
get api("/projects/#{project.id}/hooks/#{hook.id}", user3)
expect(response).to have_http_status(403)
end
end
- it "should return a 404 error if hook id is not available" do
+ it "returns a 404 error if hook id is not available" do
get api("/projects/#{project.id}/hooks/1234", user)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/hooks" do
- it "should add hook to project" do
+ it "adds hook to project" do
expect do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1)
@@ -91,22 +96,24 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
+ expect(json_response['pipeline_events']).to eq(false)
+ expect(json_response['wiki_page_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
- it "should return a 400 error if url not given" do
+ it "returns a 400 error if url not given" do
post api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(400)
end
- it "should return a 422 error if url not valid" do
+ it "returns a 422 error if url not valid" do
post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com"
expect(response).to have_http_status(422)
end
end
describe "PUT /projects/:id/hooks/:hook_id" do
- it "should update an existing project hook" do
+ it "updates an existing project hook" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user),
url: 'http://example.org', push_events: false
expect(response).to have_http_status(200)
@@ -116,49 +123,52 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
- it "should return 404 error if hook id not found" do
+ it "returns 404 error if hook id not found" do
put api("/projects/#{project.id}/hooks/1234", user), url: 'http://example.org'
expect(response).to have_http_status(404)
end
- it "should return 400 error if url is not given" do
+ it "returns 400 error if url is not given" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(400)
end
- it "should return a 422 error if url is not valid" do
+ it "returns a 422 error if url is not valid" do
put api("/projects/#{project.id}/hooks/#{hook.id}", user), url: 'ftp://example.com'
expect(response).to have_http_status(422)
end
end
describe "DELETE /projects/:id/hooks/:hook_id" do
- it "should delete hook from project" do
+ it "deletes hook from project" do
expect do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
end.to change {project.hooks.count}.by(-1)
expect(response).to have_http_status(200)
end
- it "should return success when deleting hook" do
+ it "returns success when deleting hook" do
delete api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response).to have_http_status(200)
end
- it "should return a 404 error when deleting non existent hook" do
+ it "returns a 404 error when deleting non existent hook" do
delete api("/projects/#{project.id}/hooks/42", user)
expect(response).to have_http_status(404)
end
- it "should return a 405 error if hook id not given" do
+ it "returns a 405 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
expect(response).to have_http_status(405)
end
- it "shold return a 404 if a user attempts to delete project hooks he/she does not own" do
+ it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
test_user = create(:user)
other_project = create(:project)
other_project.team << [test_user, :master]
diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb
deleted file mode 100644
index 9a7c1da4401..00000000000
--- a/spec/requests/api/project_members_spec.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'spec_helper'
-
-describe API::API, api: true do
- include ApiHelpers
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let(:project_member) { create(:project_member, :master, user: user, project: project) }
- let(:project_member2) { create(:project_member, :developer, user: user3, project: project) }
-
- describe "GET /projects/:id/members" do
- before { project_member }
- before { project_member2 }
-
- it "should return project team members" do
- get api("/projects/#{project.id}/members", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(2)
- expect(json_response.map { |u| u['username'] }).to include user.username
- end
-
- it "finds team members with query string" do
- get api("/projects/#{project.id}/members", user), query: user.username
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.count).to eq(1)
- expect(json_response.first['username']).to eq(user.username)
- end
-
- it "should return a 404 error if id not found" do
- get api("/projects/9999/members", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe "GET /projects/:id/members/:user_id" do
- before { project_member }
-
- it "should return project team member" do
- get api("/projects/#{project.id}/members/#{user.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['username']).to eq(user.username)
- expect(json_response['access_level']).to eq(ProjectMember::MASTER)
- end
-
- it "should return a 404 error if user id not found" do
- get api("/projects/#{project.id}/members/1234", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe "POST /projects/:id/members" do
- it "should add user to project team" do
- expect do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
- end.to change { ProjectMember.count }.by(1)
-
- expect(response).to have_http_status(201)
- expect(json_response['username']).to eq(user2.username)
- expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
- end
-
- it "should return a 201 status if user is already project member" do
- post api("/projects/#{project.id}/members", user),
- user_id: user2.id,
- access_level: ProjectMember::DEVELOPER
- expect do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: ProjectMember::DEVELOPER
- end.not_to change { ProjectMember.count }
-
- expect(response).to have_http_status(201)
- expect(json_response['username']).to eq(user2.username)
- expect(json_response['access_level']).to eq(ProjectMember::DEVELOPER)
- end
-
- it "should return a 400 error when user id is not given" do
- post api("/projects/#{project.id}/members", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(400)
- end
-
- it "should return a 400 error when access level is not given" do
- post api("/projects/#{project.id}/members", user), user_id: user2.id
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- post api("/projects/#{project.id}/members", user), user_id: user2.id, access_level: 1234
- expect(response).to have_http_status(422)
- end
- end
-
- describe "PUT /projects/:id/members/:user_id" do
- before { project_member2 }
-
- it "should update project team member" do
- put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(200)
- expect(json_response['username']).to eq(user3.username)
- expect(json_response['access_level']).to eq(ProjectMember::MASTER)
- end
-
- it "should return a 404 error if user_id is not found" do
- put api("/projects/#{project.id}/members/1234", user), access_level: ProjectMember::MASTER
- expect(response).to have_http_status(404)
- end
-
- it "should return a 400 error when access level is not given" do
- put api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response).to have_http_status(400)
- end
-
- it "should return a 422 error when access level is not known" do
- put api("/projects/#{project.id}/members/#{user3.id}", user), access_level: 123
- expect(response).to have_http_status(422)
- end
- end
-
- describe "DELETE /projects/:id/members/:user_id" do
- before do
- project_member
- project_member2
- end
-
- it "should remove user from project team" do
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.to change { ProjectMember.count }.by(-1)
- end
-
- it "should return 200 if team member is not part of a project" do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- end.not_to change { ProjectMember.count }
- expect(response).to have_http_status(200)
- end
-
- it "should return 200 if team member already removed" do
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- delete api("/projects/#{project.id}/members/#{user3.id}", user)
- expect(response).to have_http_status(200)
- end
-
- it "should return 200 OK when the user was not member" do
- expect do
- delete api("/projects/#{project.id}/members/1000000", user)
- end.to change { ProjectMember.count }.by(0)
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(1000000)
- expect(json_response['message']).to eq('Access revoked')
- end
-
- context 'when the user is not an admin or owner' do
- it 'can leave the project' do
- expect do
- delete api("/projects/#{project.id}/members/#{user3.id}", user3)
- end.to change { ProjectMember.count }.by(-1)
-
- expect(response).to have_http_status(200)
- expect(json_response['id']).to eq(project_member2.id)
- end
- end
- end
-end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 4ebde201941..01148f0a05e 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -17,7 +17,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:project_id/snippets/' do
- it 'all snippets available to team member' do
+ it 'returns all snippets available to team member' do
project = create(:project, :public)
user = create(:user)
project.team << [user, :developer]
@@ -30,6 +30,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response.size).to eq(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
end
it 'hides private snippets from regular user' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 8a52725a893..63f2467be63 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -43,14 +43,14 @@ describe API::API, api: true do
before { project }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects')
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return an array of projects' do
+ it 'returns an array of projects' do
get api('/projects', user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -58,21 +58,21 @@ describe API::API, api: true do
expect(json_response.first['owner']['username']).to eq(user.username)
end
- it 'should include the project labels as the tag_list' do
+ it 'includes the project labels as the tag_list' do
get api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('tag_list')
end
- it 'should include open_issues_count' do
+ it 'includes open_issues_count' do
get api('/projects', user)
expect(response.status).to eq 200
expect(json_response).to be_an Array
expect(json_response.first.keys).to include('open_issues_count')
end
- it 'should not include open_issues_count' do
+ it 'does not include open_issues_count' do
project.update_attributes( { issues_enabled: false } )
get api('/projects', user)
@@ -81,8 +81,20 @@ describe API::API, api: true do
expect(json_response.first.keys).not_to include('open_issues_count')
end
+ context 'GET /projects?simple=true' do
+ it 'returns a simplified version of all the projects' do
+ expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"]
+
+ get api('/projects?simple=true', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to match_array expected_keys
+ end
+ end
+
context 'and using search' do
- it 'should return searched project' do
+ it 'returns searched project' do
get api('/projects', user), { search: project.name }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -91,21 +103,21 @@ describe API::API, api: true do
end
context 'and using the visibility filter' do
- it 'should filter based on private visibility param' do
+ it 'filters based on private visibility param' do
get api('/projects', user), { visibility: 'private' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count)
end
- it 'should filter based on internal visibility param' do
+ it 'filters based on internal visibility param' do
get api('/projects', user), { visibility: 'internal' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count)
end
- it 'should filter based on public visibility param' do
+ it 'filters based on public visibility param' do
get api('/projects', user), { visibility: 'public' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -119,7 +131,7 @@ describe API::API, api: true do
project3
end
- it 'should return the correct order when sorted by id' do
+ it 'returns the correct order when sorted by id' do
get api('/projects', user), { order_by: 'id', sort: 'desc' }
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -133,21 +145,21 @@ describe API::API, api: true do
before { project }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects/all')
expect(response).to have_http_status(401)
end
end
context 'when authenticated as regular user' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api('/projects/all', user)
expect(response).to have_http_status(403)
end
end
context 'when authenticated as admin' do
- it 'should return an array of all projects' do
+ it 'returns an array of all projects' do
get api('/projects/all', admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -171,7 +183,7 @@ describe API::API, api: true do
user3.update_attributes(starred_projects: [project, project2, project3, public_project])
end
- it 'should return the starred projects viewable by the user' do
+ it 'returns the starred projects viewable by the user' do
get api('/projects/starred', user3)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -181,7 +193,7 @@ describe API::API, api: true do
describe 'POST /projects' do
context 'maximum number of projects reached' do
- it 'should not create new project and respond with 403' do
+ it 'does not create new project and respond with 403' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0)
expect { post api('/projects', user2), name: 'foo' }.
to change {Project.count}.by(0)
@@ -189,30 +201,31 @@ describe API::API, api: true do
end
end
- it 'should create new project without path and return 201' do
+ it 'creates new project without path and return 201' do
expect { post api('/projects', user), name: 'foo' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
end
- it 'should create last project before reaching project limit' do
+ it 'creates last project before reaching project limit' do
allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1)
post api('/projects', user2), name: 'foo'
expect(response).to have_http_status(201)
end
- it 'should not create new project without name and return 400' do
+ it 'does not create new project without name and return 400' do
expect { post api('/projects', user) }.not_to change { Project.count }
expect(response).to have_http_status(400)
end
- it "should assign attributes to project" do
+ it "assigns attributes to project" do
project = attributes_for(:project, {
path: 'camelCasePath',
description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
- wiki_enabled: false
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false
})
post api('/projects', user), project
@@ -222,55 +235,67 @@ describe API::API, api: true do
end
end
- it 'should set a project as public' do
+ it 'sets a project as public' do
project = attributes_for(:project, :public)
post api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as public using :public' do
+ it 'sets a project as public using :public' do
project = attributes_for(:project, { public: true })
post api('/projects', user), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as internal' do
+ it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as internal overriding :public' do
+ it 'sets a project as internal overriding :public' do
project = attributes_for(:project, :internal, { public: true })
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as private' do
+ it 'sets a project as private' do
project = attributes_for(:project, :private)
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should set a project as private using :public' do
+ it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post api('/projects', user), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
context 'when a visibility level is restricted' do
before do
@project = attributes_for(:project, { public: true })
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- it 'should not allow a non-admin to use a restricted visibility level' do
+ it 'does not allow a non-admin to use a restricted visibility level' do
post api('/projects', user), @project
expect(response).to have_http_status(400)
@@ -279,7 +304,7 @@ describe API::API, api: true do
)
end
- it 'should allow an admin to override restricted visibility settings' do
+ it 'allows an admin to override restricted visibility settings' do
post api('/projects', admin), @project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to(
@@ -298,7 +323,7 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it 'should respond with 400 on failure and not project' do
+ it 'responds with 400 on failure and not project' do
expect { post api("/projects/user/#{user.id}", admin) }.
not_to change { Project.count }
@@ -315,7 +340,7 @@ describe API::API, api: true do
])
end
- it 'should assign attributes to project' do
+ it 'assigns attributes to project' do
project = attributes_for(:project, {
description: FFaker::Lorem.sentence,
issues_enabled: false,
@@ -331,47 +356,59 @@ describe API::API, api: true do
end
end
- it 'should set a project as public' do
+ it 'sets a project as public' do
project = attributes_for(:project, :public)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as public using :public' 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(json_response['public']).to be_truthy
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
- it 'should set a project as internal' do
+ it 'sets a project as internal' do
project = attributes_for(:project, :internal)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as internal overriding :public' 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(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
- it 'should set a project as private' do
+ it 'sets a project as private' do
project = attributes_for(:project, :private)
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should set a project as private using :public' do
+ it 'sets a project as private using :public' do
project = attributes_for(:project, { public: false })
post api("/projects/user/#{user.id}", admin), project
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
end
describe "POST /projects/:id/uploads" do
@@ -384,7 +421,6 @@ describe API::API, api: true do
expect(json_response['alt']).to eq("dk")
expect(json_response['url']).to start_with("/uploads/")
expect(json_response['url']).to end_with("/dk.png")
- expect(json_response['is_image']).to eq(true)
end
end
@@ -433,27 +469,28 @@ describe API::API, api: true do
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)
end
- it 'should return a project by path name' do
+ 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
- it 'should return a 404 error if not found' do
+ 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 'should return a 404 error if user is not a member' do
+ 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 'should handle users with dots' do
+ it 'handles users with dots' do
dot_user = create(:user, username: 'dot.user')
project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
@@ -493,7 +530,7 @@ describe API::API, api: true do
before { project2.group.add_owner(user) }
- it 'should set the owner and return 200' do
+ it 'sets the owner and return 200' do
get api("/projects/#{project2.id}", user)
expect(response).to have_http_status(200)
@@ -534,13 +571,13 @@ describe API::API, api: true do
end
end
- it 'should return a 404 error if not found' do
+ 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 'should return a 404 error if user is not a member' do
+ 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)
@@ -550,7 +587,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/snippets' do
before { snippet }
- it 'should return an array of project snippets' do
+ it 'returns an array of project snippets' do
get api("/projects/#{project.id}/snippets", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -559,20 +596,20 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/snippets/:snippet_id' do
- it 'should return a project snippet' do
+ it 'returns a project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(snippet.title)
end
- it 'should return a 404 error if snippet id not found' do
+ it 'returns a 404 error if snippet id not found' do
get api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'POST /projects/:id/snippets' do
- it 'should create a new project snippet' do
+ it 'creates a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
title: 'api test', file_name: 'sample.rb', code: 'test',
visibility_level: '0'
@@ -580,14 +617,14 @@ describe API::API, api: true do
expect(json_response['title']).to eq('api test')
end
- it 'should return a 400 error if invalid snippet is given' do
+ it 'returns a 400 error if invalid snippet is given' do
post api("/projects/#{project.id}/snippets", user)
expect(status).to eq(400)
end
end
describe 'PUT /projects/:id/snippets/:snippet_id' do
- it 'should update an existing project snippet' do
+ it 'updates an existing project snippet' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
expect(response).to have_http_status(200)
@@ -595,7 +632,7 @@ describe API::API, api: true do
expect(snippet.reload.content).to eq('updated code')
end
- it 'should update an existing project snippet with new title' do
+ it 'updates an existing project snippet with new title' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
title: 'other api test'
expect(response).to have_http_status(200)
@@ -606,103 +643,31 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/snippets/:snippet_id' do
before { snippet }
- it 'should delete existing project snippet' do
+ it 'deletes existing project snippet' do
expect do
delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
end.to change { Snippet.count }.by(-1)
expect(response).to have_http_status(200)
end
- it 'should return 404 when deleting unknown snippet id' do
+ it 'returns 404 when deleting unknown snippet id' do
delete api("/projects/#{project.id}/snippets/1234", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/snippets/:snippet_id/raw' do
- it 'should get a raw project snippet' do
+ it 'gets a raw project snippet' do
get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
expect(response).to have_http_status(200)
end
- it 'should return a 404 error if raw project snippet not found' do
+ it 'returns a 404 error if raw project snippet not found' do
get api("/projects/#{project.id}/snippets/5555/raw", user)
expect(response).to have_http_status(404)
end
end
- describe :deploy_keys do
- let(:deploy_keys_project) { create(:deploy_keys_project, project: project) }
- let(:deploy_key) { deploy_keys_project.deploy_key }
-
- describe 'GET /projects/:id/keys' do
- before { deploy_key }
-
- it 'should return array of ssh keys' do
- get api("/projects/#{project.id}/keys", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['title']).to eq(deploy_key.title)
- end
- end
-
- describe 'GET /projects/:id/keys/:key_id' do
- it 'should return a single key' do
- get api("/projects/#{project.id}/keys/#{deploy_key.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['title']).to eq(deploy_key.title)
- end
-
- it 'should return 404 Not Found with invalid ID' do
- get api("/projects/#{project.id}/keys/404", user)
- expect(response).to have_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/keys' do
- it 'should not create an invalid ssh key' do
- post api("/projects/#{project.id}/keys", user), { title: 'invalid key' }
- 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
-
- it 'should not create a key without title' do
- post api("/projects/#{project.id}/keys", user), key: 'some key'
- 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)'
- ])
- end
-
- it 'should create new ssh key' do
- key_attrs = attributes_for :key
- expect do
- post api("/projects/#{project.id}/keys", user), key_attrs
- end.to change{ project.deploy_keys.count }.by(1)
- end
- end
-
- describe 'DELETE /projects/:id/keys/:key_id' do
- before { deploy_key }
-
- it 'should delete existing key' do
- expect do
- delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user)
- end.to change{ project.deploy_keys.count }.by(-1)
- end
-
- it 'should return 404 Not Found with invalid ID' do
- delete api("/projects/#{project.id}/keys/404", user)
- expect(response).to have_http_status(404)
- end
- end
- end
-
describe :fork_admin do
let(:project_fork_target) { create(:project) }
let(:project_fork_source) { create(:project, :public) }
@@ -710,12 +675,12 @@ describe API::API, api: true do
describe 'POST /projects/:id/fork/:forked_from_id' do
let(:new_project_fork_source) { create(:project, :public) }
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
expect(response).to have_http_status(403)
end
- it 'should allow project to be forked from an existing project' do
+ it 'allows project to be forked from an existing project' do
expect(project_fork_target.forked?).not_to be_truthy
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
expect(response).to have_http_status(201)
@@ -725,12 +690,12 @@ describe API::API, api: true do
expect(project_fork_target.forked?).to be_truthy
end
- it 'should fail if forked_from project which does not exist' do
+ it 'fails if forked_from project which does not exist' do
post api("/projects/#{project_fork_target.id}/fork/9999", admin)
expect(response).to have_http_status(404)
end
- it 'should fail with 409 if already forked' do
+ it 'fails with 409 if already forked' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id)
@@ -743,7 +708,7 @@ describe API::API, api: true do
end
describe 'DELETE /projects/:id/fork' do
- it "shouldn't be visible to users outside group" do
+ it "is not visible to users outside group" do
delete api("/projects/#{project_fork_target.id}/fork", user)
expect(response).to have_http_status(404)
end
@@ -756,12 +721,12 @@ describe API::API, api: true do
project_fork_target.group.add_developer user2
end
- it 'should be forbidden to non-owner users' do
+ it 'is forbidden to non-owner users' do
delete api("/projects/#{project_fork_target.id}/fork", user2)
expect(response).to have_http_status(403)
end
- it 'should make forked project unforked' do
+ it 'makes forked project unforked' do
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
project_fork_target.reload
expect(project_fork_target.forked_from_project).not_to be_nil
@@ -773,7 +738,7 @@ describe API::API, api: true do
expect(project_fork_target.forked?).not_to be_truthy
end
- it 'should be idempotent if not forked' 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)
@@ -786,7 +751,7 @@ describe API::API, api: true do
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
- it "should share project with group" do
+ it "shares project with group" do
expect do
post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
end.to change { ProjectGroupLink.count }.by(1)
@@ -796,23 +761,23 @@ describe API::API, api: true do
expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
end
- it "should return a 400 error when group id is not given" 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
end
- it "should return a 400 error when access level is not given" do
+ 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
end
- it "should return a 400 error when sharing is disabled" do
+ 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
end
- it "should return a 409 error when wrong params passed" do
+ it "returns a 409 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'
@@ -832,14 +797,14 @@ describe API::API, api: true do
let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/projects/search/#{query}")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return an array of projects' 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
@@ -849,7 +814,7 @@ describe API::API, api: true do
end
context 'when authenticated as a different user' do
- it 'should return matching public projects' 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
@@ -870,7 +835,7 @@ describe API::API, api: true do
before { project_member2 }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}"), project_param
expect(response).to have_http_status(401)
@@ -878,7 +843,7 @@ describe API::API, api: true do
end
context 'when authenticated as project owner' do
- it 'should update name' do
+ it 'updates name' do
project_param = { name: 'bar' }
put api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(200)
@@ -887,7 +852,7 @@ describe API::API, api: true do
end
end
- it 'should update visibility_level' do
+ it 'updates visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
@@ -896,7 +861,7 @@ describe API::API, api: true do
end
end
- it 'should update visibility_level from public to private' do
+ it 'updates visibility_level from public to private' do
project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { public: false }
@@ -908,14 +873,14 @@ describe API::API, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
- it 'should not update name to existing name' do
+ it 'does not update name to existing name' do
project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['name']).to eq(['has already been taken'])
end
- it 'should update path & name to existing path & name in different namespace' do
+ it 'updates path & name to existing path & name in different namespace' do
project_param = { path: project4.path, name: project4.name }
put api("/projects/#{project3.id}", user), project_param
expect(response).to have_http_status(200)
@@ -926,7 +891,7 @@ describe API::API, api: true do
end
context 'when authenticated as project master' do
- it 'should update path' do
+ it 'updates path' do
project_param = { path: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(200)
@@ -935,7 +900,7 @@ describe API::API, api: true do
end
end
- it 'should update other attributes' do
+ it 'updates other attributes' do
project_param = { issues_enabled: true,
wiki_enabled: true,
snippets_enabled: true,
@@ -949,20 +914,20 @@ describe API::API, api: true do
end
end
- it 'should not update path to existing path' do
+ it 'does not update path to existing path' do
project_param = { path: project.path }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(400)
expect(json_response['message']['path']).to eq(['has already been taken'])
end
- it 'should not update name' do
+ it 'does not update name' do
project_param = { name: 'bar' }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
end
- it 'should not update visibility_level' do
+ it 'does not update visibility_level' do
project_param = { visibility_level: 20 }
put api("/projects/#{project3.id}", user4), project_param
expect(response).to have_http_status(403)
@@ -970,7 +935,7 @@ describe API::API, api: true do
end
context 'when authenticated as project developer' do
- it 'should not update other attributes' do
+ it 'does not update other attributes' do
project_param = { path: 'bar',
issues_enabled: true,
wiki_enabled: true,
@@ -1105,36 +1070,36 @@ describe API::API, api: true do
describe 'DELETE /projects/:id' do
context 'when authenticated as user' do
- it 'should remove project' do
+ it 'removes project' do
delete api("/projects/#{project.id}", user)
expect(response).to have_http_status(200)
end
- it 'should not remove a project if not an owner' do
+ it 'does not remove a project if not an owner' do
user3 = create(:user)
project.team << [user3, :developer]
delete api("/projects/#{project.id}", user3)
expect(response).to have_http_status(403)
end
- it 'should not remove a non existing project' do
+ it 'does not remove a non existing project' do
delete api('/projects/1328', user)
expect(response).to have_http_status(404)
end
- it 'should not remove a project not attached to user' do
+ it 'does not remove a project not attached to user' do
delete api("/projects/#{project.id}", user2)
expect(response).to have_http_status(404)
end
end
context 'when authenticated as admin' do
- it 'should remove any existing project' do
+ it 'removes any existing project' do
delete api("/projects/#{project.id}", admin)
expect(response).to have_http_status(200)
end
- it 'should not remove a non existing project' do
+ it 'does not remove a non existing project' do
delete api('/projects/1328', admin)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 5890e9c9d3d..80a856a6e90 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true do
context "authorized user" do
before { project.team << [user2, :reporter] }
- it "should return project commits" do
+ it "returns project commits" do
get api("/projects/#{project.id}/repository/tree", user)
expect(response).to have_http_status(200)
@@ -26,7 +26,7 @@ describe API::API, api: true do
expect(json_response.first['mode']).to eq('040000')
end
- it 'should return a 404 for unknown ref' do
+ it 'returns a 404 for unknown ref' do
get api("/projects/#{project.id}/repository/tree?ref_name=foo", user)
expect(response).to have_http_status(404)
@@ -36,7 +36,7 @@ describe API::API, api: true do
end
context "unauthorized user" do
- it "should not return project commits" do
+ it "does not return project commits" do
get api("/projects/#{project.id}/repository/tree")
expect(response).to have_http_status(401)
end
@@ -44,41 +44,41 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/blobs/:sha" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.md", user)
expect(response).to have_http_status(200)
end
- it "should return 404 for invalid branch_name" do
+ it "returns 404 for invalid branch_name" do
get api("/projects/#{project.id}/repository/blobs/invalid_branch_name?filepath=README.md", user)
expect(response).to have_http_status(404)
end
- it "should return 404 for invalid file" do
+ it "returns 404 for invalid file" do
get api("/projects/#{project.id}/repository/blobs/master?filepath=README.invalid", user)
expect(response).to have_http_status(404)
end
- it "should return a 400 error if filepath is missing" do
+ it "returns a 400 error if filepath is missing" do
get api("/projects/#{project.id}/repository/blobs/master", user)
expect(response).to have_http_status(400)
end
end
describe "GET /projects/:id/repository/commits/:sha/blob" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/commits/master/blob?filepath=README.md", user)
expect(response).to have_http_status(200)
end
end
describe "GET /projects/:id/repository/raw_blobs/:sha" do
- it "should get the raw file contents" do
+ it "gets the raw file contents" do
get api("/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}", user)
expect(response).to have_http_status(200)
end
- it 'should return a 404 for unknown blob' do
+ it 'returns a 404 for unknown blob' do
get api("/projects/#{project.id}/repository/raw_blobs/123456", user)
expect(response).to have_http_status(404)
@@ -88,7 +88,7 @@ describe API::API, api: true do
end
describe "GET /projects/:id/repository/archive(.:format)?:sha" do
- it "should get the archive" do
+ it "gets the archive" do
get api("/projects/#{project.id}/repository/archive", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -97,7 +97,7 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
end
- it "should get the archive.zip" do
+ it "gets the archive.zip" do
get api("/projects/#{project.id}/repository/archive.zip", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -106,7 +106,7 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
end
- it "should get the archive.tar.bz2" do
+ it "gets the archive.tar.bz2" do
get api("/projects/#{project.id}/repository/archive.tar.bz2", user)
repo_name = project.repository.name.gsub("\.git", "")
expect(response).to have_http_status(200)
@@ -115,28 +115,28 @@ describe API::API, api: true do
expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
end
- it "should return 404 for invalid sha" do
+ it "returns 404 for invalid sha" do
get api("/projects/#{project.id}/repository/archive/?sha=xxx", user)
expect(response).to have_http_status(404)
end
end
describe 'GET /projects/:id/repository/compare' do
- it "should compare branches" do
+ it "compares branches" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'feature'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare tags" do
+ it "compares tags" do
get api("/projects/#{project.id}/repository/compare", user), from: 'v1.0.0', to: 'v1.1.0'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare commits" do
+ it "compares commits" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.id, to: sample_commit.parent_id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
@@ -144,14 +144,14 @@ describe API::API, api: true do
expect(json_response['compare_same_ref']).to be_falsey
end
- it "should compare commits in reverse order" do
+ it "compares commits in reverse order" do
get api("/projects/#{project.id}/repository/compare", user), from: sample_commit.parent_id, to: sample_commit.id
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_present
expect(json_response['diffs']).to be_present
end
- it "should compare same refs" do
+ it "compares same refs" do
get api("/projects/#{project.id}/repository/compare", user), from: 'master', to: 'master'
expect(response).to have_http_status(200)
expect(json_response['commits']).to be_empty
@@ -161,7 +161,7 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/repository/contributors' do
- it 'should return valid data' do
+ it 'returns valid data' do
get api("/projects/#{project.id}/repository/contributors", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 00a3c917b6a..f46f016135e 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -35,7 +35,7 @@ describe API::Runners, api: true do
describe 'GET /runners' do
context 'authorized user' do
- it 'should return user available runners' do
+ it 'returns user available runners' do
get api('/runners', user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -44,7 +44,7 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should filter runners by scope' do
+ it 'filters runners by scope' do
get api('/runners?scope=active', user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -53,14 +53,14 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should avoid filtering if scope is invalid' do
+ it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', user)
expect(response).to have_http_status(400)
end
end
context 'unauthorized user' do
- it 'should not return runners' do
+ it 'does not return runners' do
get api('/runners')
expect(response).to have_http_status(401)
@@ -71,7 +71,7 @@ describe API::Runners, api: true do
describe 'GET /runners/all' do
context 'authorized user' do
context 'with admin privileges' do
- it 'should return all runners' do
+ it 'returns all runners' do
get api('/runners/all', admin)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -82,14 +82,14 @@ describe API::Runners, api: true do
end
context 'without admin privileges' do
- it 'should not return runners list' do
+ it 'does not return runners list' do
get api('/runners/all', user)
expect(response).to have_http_status(403)
end
end
- it 'should filter runners by scope' do
+ it 'filters runners by scope' do
get api('/runners/all?scope=specific', admin)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -98,14 +98,14 @@ describe API::Runners, api: true do
expect(shared).to be_falsey
end
- it 'should avoid filtering if scope is invalid' do
+ it 'avoids filtering if scope is invalid' do
get api('/runners?scope=unknown', admin)
expect(response).to have_http_status(400)
end
end
context 'unauthorized user' do
- it 'should not return runners' do
+ it 'does not return runners' do
get api('/runners')
expect(response).to have_http_status(401)
@@ -116,7 +116,7 @@ describe API::Runners, api: true do
describe 'GET /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{shared_runner.id}", admin)
expect(response).to have_http_status(200)
@@ -125,7 +125,7 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{specific_runner.id}", admin)
expect(response).to have_http_status(200)
@@ -133,7 +133,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
get api('/runners/9999', admin)
expect(response).to have_http_status(404)
@@ -142,7 +142,7 @@ describe API::Runners, api: true do
context "runner project's administrative user" do
context 'when runner is not shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{specific_runner.id}", user)
expect(response).to have_http_status(200)
@@ -151,7 +151,7 @@ describe API::Runners, api: true do
end
context 'when runner is shared' do
- it "should return runner's details" do
+ it "returns runner's details" do
get api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(200)
@@ -161,7 +161,7 @@ describe API::Runners, api: true do
end
context 'other authorized user' do
- it "should not return runner's details" do
+ it "does not return runner's details" do
get api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
@@ -169,7 +169,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not return runner's details" do
+ it "does not return runner's details" do
get api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -180,7 +180,7 @@ describe API::Runners, api: true do
describe 'PUT /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it 'should update runner' do
+ it 'updates runner' do
description = shared_runner.description
active = shared_runner.active
@@ -201,7 +201,7 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should update runner' do
+ it 'updates runner' do
description = specific_runner.description
update_runner(specific_runner.id, admin, description: 'test')
specific_runner.reload
@@ -212,7 +212,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
update_runner(9999, admin, description: 'test')
expect(response).to have_http_status(404)
@@ -225,7 +225,7 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
- it 'should not update runner' do
+ it 'does not update runner' do
put api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(403)
@@ -233,13 +233,13 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should not update runner without access to it' do
+ it 'does not update runner without access to it' do
put api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
end
- it 'should update runner with access to it' do
+ it 'updates runner with access to it' do
description = specific_runner.description
put api("/runners/#{specific_runner.id}", admin), description: 'test'
specific_runner.reload
@@ -252,7 +252,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
put api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -263,7 +263,7 @@ describe API::Runners, api: true do
describe 'DELETE /runners/:id' do
context 'admin user' do
context 'when runner is shared' do
- it 'should delete runner' do
+ it 'deletes runner' do
expect do
delete api("/runners/#{shared_runner.id}", admin)
end.to change{ Ci::Runner.shared.count }.by(-1)
@@ -272,14 +272,14 @@ describe API::Runners, api: true do
end
context 'when runner is not shared' do
- it 'should delete unused runner' do
+ it 'deletes unused runner' do
expect do
delete api("/runners/#{unused_specific_runner.id}", admin)
end.to change{ Ci::Runner.specific.count }.by(-1)
expect(response).to have_http_status(200)
end
- it 'should delete used runner' do
+ it 'deletes used runner' do
expect do
delete api("/runners/#{specific_runner.id}", admin)
end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -287,7 +287,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 if runner does not exists' do
+ it 'returns 404 if runner does not exists' do
delete api('/runners/9999', admin)
expect(response).to have_http_status(404)
@@ -296,24 +296,24 @@ describe API::Runners, api: true do
context 'authorized user' do
context 'when runner is shared' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
delete api("/runners/#{shared_runner.id}", user)
expect(response).to have_http_status(403)
end
end
context 'when runner is not shared' do
- it 'should not delete runner without access to it' do
+ it 'does not delete runner without access to it' do
delete api("/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
end
- it 'should not delete runner with more than one associated project' do
+ it 'does not delete runner with more than one associated project' do
delete api("/runners/#{two_projects_runner.id}", user)
expect(response).to have_http_status(403)
end
- it 'should delete runner for one owned project' do
+ it 'deletes runner for one owned project' do
expect do
delete api("/runners/#{specific_runner.id}", user)
end.to change{ Ci::Runner.specific.count }.by(-1)
@@ -323,7 +323,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not delete runner' do
+ it 'does not delete runner' do
delete api("/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
@@ -333,7 +333,7 @@ describe API::Runners, api: true do
describe 'GET /projects/:id/runners' do
context 'authorized user with master privileges' do
- it "should return project's runners" do
+ it "returns project's runners" do
get api("/projects/#{project.id}/runners", user)
shared = json_response.any?{ |r| r['is_shared'] }
@@ -344,7 +344,7 @@ describe API::Runners, api: true do
end
context 'authorized user without master privileges' do
- it "should not return project's runners" do
+ it "does not return project's runners" do
get api("/projects/#{project.id}/runners", user2)
expect(response).to have_http_status(403)
@@ -352,7 +352,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not return project's runners" do
+ it "does not return project's runners" do
get api("/projects/#{project.id}/runners")
expect(response).to have_http_status(401)
@@ -368,21 +368,21 @@ describe API::Runners, api: true do
end
end
- it 'should enable specific runner' do
+ it 'enables specific runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(+1)
expect(response).to have_http_status(201)
end
- it 'should avoid changes when enabling already enabled runner' do
+ it 'avoids changes when enabling already enabled runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change{ project.runners.count }.by(0)
expect(response).to have_http_status(409)
end
- it 'should not enable locked runner' do
+ it 'does not enable locked runner' do
specific_runner2.update(locked: true)
expect do
@@ -392,14 +392,14 @@ describe API::Runners, api: true do
expect(response).to have_http_status(403)
end
- it 'should not enable shared runner' do
+ it 'does not enable shared runner' do
post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id
expect(response).to have_http_status(403)
end
context 'user is admin' do
- it 'should enable any specific runner' do
+ it 'enables any specific runner' do
expect do
post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id
end.to change{ project.runners.count }.by(+1)
@@ -408,14 +408,14 @@ describe API::Runners, api: true do
end
context 'user is not admin' do
- it 'should not enable runner without access to' do
+ it 'does not enable runner without access to' do
post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id
expect(response).to have_http_status(403)
end
end
- it 'should raise an error when no runner_id param is provided' do
+ it 'raises an error when no runner_id param is provided' do
post api("/projects/#{project.id}/runners", admin)
expect(response).to have_http_status(400)
@@ -423,7 +423,7 @@ describe API::Runners, api: true do
end
context 'authorized user without permissions' do
- it 'should not enable runner' do
+ it 'does not enable runner' do
post api("/projects/#{project.id}/runners", user2)
expect(response).to have_http_status(403)
@@ -431,7 +431,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it 'should not enable runner' do
+ it 'does not enable runner' do
post api("/projects/#{project.id}/runners")
expect(response).to have_http_status(401)
@@ -442,7 +442,7 @@ describe API::Runners, api: true do
describe 'DELETE /projects/:id/runners/:runner_id' do
context 'authorized user' do
context 'when runner have more than one associated projects' do
- it "should disable project's runner" do
+ it "disables project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user)
end.to change{ project.runners.count }.by(-1)
@@ -451,7 +451,7 @@ describe API::Runners, api: true do
end
context 'when runner have one associated projects' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
expect do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user)
end.to change{ project.runners.count }.by(0)
@@ -459,7 +459,7 @@ describe API::Runners, api: true do
end
end
- it 'should return 404 is runner is not found' do
+ it 'returns 404 is runner is not found' do
delete api("/projects/#{project.id}/runners/9999", user)
expect(response).to have_http_status(404)
@@ -467,7 +467,7 @@ describe API::Runners, api: true do
end
context 'authorized user without permissions' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2)
expect(response).to have_http_status(403)
@@ -475,7 +475,7 @@ describe API::Runners, api: true do
end
context 'unauthorized user' do
- it "should not disable project's runner" do
+ it "does not disable project's runner" do
delete api("/projects/#{project.id}/runners/#{specific_runner.id}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index a2446e12804..375671bca4c 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -11,13 +11,13 @@ describe API::API, api: true do
describe "PUT /projects/:id/services/#{service.dasherize}" do
include_context service
- it "should update #{service} settings" do
+ it "updates #{service} settings" do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200)
end
- it "should return if required fields missing" do
+ it "returns if required fields missing" do
attrs = service_attrs
required_attributes = service_attrs_list.select do |attr|
@@ -32,7 +32,7 @@ describe API::API, api: true do
attrs.delete(required_attributes.sample)
expected_code = 400
end
-
+
put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs
expect(response.status).to eq(expected_code)
@@ -42,7 +42,7 @@ describe API::API, api: true do
describe "DELETE /projects/:id/services/#{service.dasherize}" do
include_context service
- it "should delete #{service}" do
+ it "deletes #{service}" do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_http_status(200)
@@ -62,29 +62,29 @@ describe API::API, api: true do
service_object.save
end
- it 'should return authentication error when unauthenticated' do
+ it 'returns authentication error when unauthenticated' do
get api("/projects/#{project.id}/services/#{dashed_service}")
expect(response).to have_http_status(401)
end
-
- it "should return all properties of service #{service} when authenticated as admin" do
+
+ it "returns all properties of service #{service} when authenticated as admin" do
get api("/projects/#{project.id}/services/#{dashed_service}", admin)
-
+
expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list.map)
end
- it "should return properties of service #{service} other than passwords when authenticated as project owner" do
+ it "returns properties of service #{service} other than passwords when authenticated as project owner" do
get api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response).to have_http_status(200)
expect(json_response['properties'].keys.map(&:to_sym)).to match_array(service_attrs_list_without_passwords)
end
- it "should return error when authenticated but not a project owner" do
+ it "returns error when authenticated but not a project owner" do
project.team << [user2, :developer]
get api("/projects/#{project.id}/services/#{dashed_service}", user2)
-
+
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index c15b7ff9792..acad1365ace 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -7,7 +7,7 @@ describe API::API, api: true do
describe "POST /session" do
context "when valid password" do
- it "should return private token" do
+ it "returns private token" do
post api("/session"), email: user.email, password: '12345678'
expect(response).to have_http_status(201)
@@ -17,10 +17,21 @@ describe API::API, api: true do
expect(json_response['can_create_project']).to eq(user.can_create_project?)
expect(json_response['can_create_group']).to eq(user.can_create_group?)
end
+
+ context 'with 2FA enabled' do
+ it 'rejects sign in attempts' do
+ user = create(:user, :two_factor)
+
+ post api('/session'), email: user.email, password: user.password
+
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled.')
+ end
+ end
end
context 'when email has case-typo and password is valid' do
- it 'should return private token' do
+ it 'returns private token' do
post api('/session'), email: user.email.upcase, password: '12345678'
expect(response.status).to eq 201
@@ -33,7 +44,7 @@ describe API::API, api: true do
end
context 'when login has case-typo and password is valid' do
- it 'should return private token' do
+ it 'returns private token' do
post api('/session'), login: user.username.upcase, password: '12345678'
expect(response.status).to eq 201
@@ -46,7 +57,7 @@ describe API::API, api: true do
end
context "when invalid password" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), email: user.email, password: '123'
expect(response).to have_http_status(401)
@@ -56,7 +67,7 @@ describe API::API, api: true do
end
context "when empty password" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), email: user.email
expect(response).to have_http_status(401)
@@ -66,7 +77,7 @@ describe API::API, api: true do
end
context "when empty name" do
- it "should return authentication error" do
+ it "returns authentication error" do
post api("/session"), password: user.password
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 684c2cd8e24..54d096e8b7f 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -7,7 +7,7 @@ describe API::API, 'Settings', api: true do
let(:admin) { create(:admin) }
describe "GET /application/settings" do
- it "should return application settings" do
+ it "returns application settings" do
get api("/application/settings", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Hash
@@ -23,7 +23,7 @@ describe API::API, 'Settings', api: true do
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
- it "should update application settings" do
+ it "updates application settings" do
put api("/application/settings", admin),
default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index cf66f261ade..1ce2658569e 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -11,21 +11,21 @@ describe API::API, api: true do
describe "GET /hooks" do
context "when no user" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/hooks")
expect(response).to have_http_status(401)
end
end
context "when not an admin" do
- it "should return forbidden error" do
+ it "returns forbidden error" do
get api("/hooks", user)
expect(response).to have_http_status(403)
end
end
context "when authenticated as admin" do
- it "should return an array of hooks" do
+ it "returns an array of hooks" do
get api("/hooks", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -35,18 +35,18 @@ describe API::API, api: true do
end
describe "POST /hooks" do
- it "should create new hook" do
+ it "creates new hook" do
expect do
post api("/hooks", admin), url: 'http://example.com'
end.to change { SystemHook.count }.by(1)
end
- it "should respond with 400 if url not given" do
+ it "responds with 400 if url not given" do
post api("/hooks", admin)
expect(response).to have_http_status(400)
end
- it "should not create new hook without url" do
+ it "does not create new hook without url" do
expect do
post api("/hooks", admin)
end.not_to change { SystemHook.count }
@@ -54,26 +54,26 @@ describe API::API, api: true do
end
describe "GET /hooks/:id" do
- it "should return hook by id" do
+ it "returns hook by id" do
get api("/hooks/#{hook.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['event_name']).to eq('project_create')
end
- it "should return 404 on failure" do
+ it "returns 404 on failure" do
get api("/hooks/404", admin)
expect(response).to have_http_status(404)
end
end
describe "DELETE /hooks/:id" do
- it "should delete a hook" do
+ it "deletes a hook" do
expect do
delete api("/hooks/#{hook.id}", admin)
end.to change { SystemHook.count }.by(-1)
end
- it "should return success if hook id not found" do
+ it "returns success if hook id not found" do
delete api("/hooks/12345", admin)
expect(response).to have_http_status(200)
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fa700ab7343..d563883cd47 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -16,7 +16,7 @@ describe API::API, api: true do
let(:description) { 'Awesome release!' }
context 'without releases' do
- it "should return an array of project tags" do
+ it "returns an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -30,7 +30,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it "should return an array of project tags with release info" do
+ it "returns an array of project tags with release info" do
get api("/projects/#{project.id}/repository/tags", user)
expect(response).to have_http_status(200)
@@ -61,7 +61,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/repository/tags' do
context 'lightweight tags' do
- it 'should create a new tag' do
+ it 'creates a new tag' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v7.0.1',
ref: 'master'
@@ -72,7 +72,7 @@ describe API::API, api: true do
end
context 'lightweight tags with release notes' do
- it 'should create a new tag' do
+ it 'creates a new tag' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v7.0.1',
ref: 'master',
@@ -92,13 +92,13 @@ describe API::API, api: true do
end
context 'delete tag' do
- it 'should delete an existing tag' do
+ it 'deletes an existing tag' do
delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
expect(response).to have_http_status(200)
expect(json_response['tag_name']).to eq(tag_name)
end
- it 'should raise 404 if the tag does not exist' do
+ it 'raises 404 if the tag does not exist' do
delete api("/projects/#{project.id}/repository/tags/foobar", user)
expect(response).to have_http_status(404)
end
@@ -106,7 +106,7 @@ describe API::API, api: true do
end
context 'annotated tag' do
- it 'should create a new annotated tag' do
+ it 'creates a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
repo_path = project.repository.path_to_repo
system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
@@ -123,14 +123,14 @@ describe API::API, api: true do
end
end
- it 'should deny for user without push access' do
+ it 'denies for user without push access' do
post api("/projects/#{project.id}/repository/tags", user2),
tag_name: 'v1.9.0',
ref: '621491c677087aa243f165eab467bfdfbee00be1'
expect(response).to have_http_status(403)
end
- it 'should return 400 if tag name is invalid' do
+ it 'returns 400 if tag name is invalid' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v 1.0.0',
ref: 'master'
@@ -138,7 +138,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag name invalid')
end
- it 'should return 400 if tag already exists' do
+ it 'returns 400 if tag already exists' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'v8.0.0',
ref: 'master'
@@ -150,7 +150,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag v8.0.0 already exists')
end
- it 'should return 400 if ref name is invalid' do
+ it 'returns 400 if ref name is invalid' do
post api("/projects/#{project.id}/repository/tags", user),
tag_name: 'mytag',
ref: 'foo'
@@ -163,7 +163,7 @@ describe API::API, api: true do
let(:tag_name) { project.repository.tag_names.first }
let(:description) { 'Awesome release!' }
- it 'should create description for existing git tag' do
+ it 'creates description for existing git tag' do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
@@ -172,7 +172,7 @@ describe API::API, api: true do
expect(json_response['description']).to eq(description)
end
- it 'should return 404 if the tag does not exist' do
+ it 'returns 404 if the tag does not exist' do
post api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: description
@@ -186,7 +186,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it 'should return 409 if there is already a release' do
+ it 'returns 409 if there is already a release' do
post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: description
@@ -207,7 +207,7 @@ describe API::API, api: true do
release.update_attributes(description: description)
end
- it 'should update the release description' do
+ it 'updates the release description' do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
@@ -217,7 +217,7 @@ describe API::API, api: true do
end
end
- it 'should return 404 if the tag does not exist' do
+ it 'returns 404 if the tag does not exist' do
put api("/projects/#{project.id}/repository/tags/foobar/release", user),
description: new_description
@@ -225,7 +225,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Tag does not exist')
end
- it 'should return 404 if the release does not exist' do
+ it 'returns 404 if the release does not exist' do
put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
description: new_description
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 68d0f41b489..5bd5b861792 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -3,50 +3,53 @@ require 'spec_helper'
describe API::Templates, api: true do
include ApiHelpers
- describe 'the Template Entity' do
- before { get api('/gitignores/Ruby') }
+ context 'global templates' do
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
- describe 'the TemplateList Entity' do
- before { get api('/gitignores') }
+ describe 'the TemplateList Entity' do
+ before { get api('/gitignores') }
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
- end
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
- context 'requesting gitignores' do
- describe 'GET /gitignores' do
- it 'returns a list of available gitignore templates' do
- get api('/gitignores')
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
end
end
- end
- context 'requesting gitlab-ci-ymls' do
- describe 'GET /gitlab_ci_ymls' do
- it 'returns a list of available gitlab_ci_ymls' do
- get api('/gitlab_ci_ymls')
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).not_to be_nil
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
end
end
- end
- describe 'GET /gitlab_ci_ymls/Ruby' do
- it 'adds a disclaimer on the top' do
- get api('/gitlab_ci_ymls/Ruby')
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
- expect(response).to have_http_status(200)
- expect(json_response['content']).to start_with("# This file is a template,")
+ expect(response).to have_http_status(200)
+ expect(json_response['name']).not_to be_nil
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
end
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index 92a4fa216cd..887a2ba5b84 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -117,6 +117,12 @@ describe API::Todos, api: true do
expect(response.status).to eq(200)
expect(pending_1.reload).to be_done
end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete api("/todos/#{pending_1.id}", john_doe)
+ end
end
end
@@ -134,12 +140,17 @@ describe API::Todos, api: true do
delete api('/todos', john_doe)
expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect(response.body).to eq('3')
expect(pending_1.reload).to be_done
expect(pending_2.reload).to be_done
expect(pending_3.reload).to be_done
end
+
+ it 'updates todos cache' do
+ expect_any_instance_of(User).to receive(:update_todos_count_cache).and_call_original
+
+ delete api("/todos", john_doe)
+ end
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 8992996c30a..82bba1ce8a4 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -27,17 +27,17 @@ describe API::API do
end
context 'Handles errors' do
- it 'should return bad request if token is missing' do
+ it 'returns bad request if token is missing' do
post api("/projects/#{project.id}/trigger/builds"), ref: 'master'
expect(response).to have_http_status(400)
end
- it 'should return not found if project is not found' do
+ it 'returns not found if project is not found' do
post api('/projects/0/trigger/builds'), options.merge(ref: 'master')
expect(response).to have_http_status(404)
end
- it 'should return unauthorized if token is for different project' do
+ it 'returns unauthorized if token is for different project' do
post api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
expect(response).to have_http_status(401)
end
@@ -46,14 +46,15 @@ describe API::API do
context 'Have a commit' do
let(:pipeline) { project.pipelines.last }
- it 'should create builds' do
+ it 'creates builds' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'master')
expect(response).to have_http_status(201)
pipeline.builds.reload
- expect(pipeline.builds.size).to eq(2)
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
end
- it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ 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)
expect(json_response['message']).to eq('No builds created')
@@ -64,19 +65,19 @@ describe API::API do
{ 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
end
- it 'should validate variables to be a hash' 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')
end
- it 'should validate variables needs to be a map of key-valued strings' do
+ it 'validates variables needs to be a map of key-valued strings' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: { key: %w(1 2) }, ref: 'master')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
- it 'create trigger request with variables' do
+ it 'creates trigger request with variables' do
post api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
expect(response).to have_http_status(201)
pipeline.builds.reload
@@ -88,7 +89,7 @@ describe API::API do
describe 'GET /projects/:id/triggers' do
context 'authenticated user with valid permissions' do
- it 'should return list of triggers' do
+ it 'returns list of triggers' do
get api("/projects/#{project.id}/triggers", user)
expect(response).to have_http_status(200)
@@ -98,7 +99,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers", user2)
expect(response).to have_http_status(403)
@@ -106,7 +107,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers")
expect(response).to have_http_status(401)
@@ -116,14 +117,14 @@ describe API::API do
describe 'GET /projects/:id/triggers/:token' do
context 'authenticated user with valid permissions' do
- it 'should return trigger details' do
+ it 'returns trigger details' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_a(Hash)
end
- it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
get api("/projects/#{project.id}/triggers/abcdef012345", user)
expect(response).to have_http_status(404)
@@ -131,7 +132,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
expect(response).to have_http_status(403)
@@ -139,7 +140,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not return triggers list' do
+ it 'does not return triggers list' do
get api("/projects/#{project.id}/triggers/#{trigger.token}")
expect(response).to have_http_status(401)
@@ -149,7 +150,7 @@ describe API::API do
describe 'POST /projects/:id/triggers' do
context 'authenticated user with valid permissions' do
- it 'should create trigger' do
+ it 'creates trigger' do
expect do
post api("/projects/#{project.id}/triggers", user)
end.to change{project.triggers.count}.by(1)
@@ -160,7 +161,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not create trigger' do
+ it 'does not create trigger' do
post api("/projects/#{project.id}/triggers", user2)
expect(response).to have_http_status(403)
@@ -168,7 +169,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not create trigger' do
+ it 'does not create trigger' do
post api("/projects/#{project.id}/triggers")
expect(response).to have_http_status(401)
@@ -178,14 +179,14 @@ describe API::API do
describe 'DELETE /projects/:id/triggers/:token' do
context 'authenticated user with valid permissions' do
- it 'should delete trigger' do
+ it 'deletes trigger' do
expect do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
end.to change{project.triggers.count}.by(-1)
expect(response).to have_http_status(200)
end
- it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ it 'responds with 404 Not Found if requesting non-existing trigger' do
delete api("/projects/#{project.id}/triggers/abcdef012345", user)
expect(response).to have_http_status(404)
@@ -193,7 +194,7 @@ describe API::API do
end
context 'authenticated user with invalid permissions' do
- it 'should not delete trigger' do
+ it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
expect(response).to have_http_status(403)
@@ -201,7 +202,7 @@ describe API::API do
end
context 'unauthenticated user' do
- it 'should not delete trigger' do
+ it 'does not delete trigger' do
delete api("/projects/#{project.id}/triggers/#{trigger.token}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index e43e3e269bf..0bbba64a6d5 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -13,7 +13,7 @@ describe API::API, api: true do
describe "GET /users" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/users")
expect(response).to have_http_status(401)
end
@@ -38,7 +38,7 @@ describe API::API, api: true do
end
end
- it "should return an array of users" do
+ it "returns an array of users" do
get api("/users", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -48,7 +48,7 @@ describe API::API, api: true do
end['username']).to eq(username)
end
- it "should return one user" do
+ it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -57,7 +57,7 @@ describe API::API, api: true do
end
context "when admin" do
- it "should return an array of users" do
+ it "returns an array of users" do
get api("/users", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -72,24 +72,24 @@ describe API::API, api: true do
end
describe "GET /users/:id" do
- it "should return a user by id" do
+ it "returns a user by id" do
get api("/users/#{user.id}", user)
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
end
- it "should return a 401 if unauthenticated" do
+ it "returns a 401 if unauthenticated" do
get api("/users/9998")
expect(response).to have_http_status(401)
end
- it "should return a 404 error if user id not found" 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')
end
- it "should return a 404 if invalid ID" do
+ it "returns a 404 if invalid ID" do
get api("/users/1ASDF", user)
expect(response).to have_http_status(404)
end
@@ -98,13 +98,13 @@ describe API::API, api: true do
describe "POST /users" do
before{ admin }
- it "should create user" do
+ it "creates user" do
expect do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
end.to change { User.count }.by(1)
end
- it "should create user with correct attributes" do
+ it "creates user with correct attributes" do
post api('/users', admin), attributes_for(:user, admin: true, can_create_group: true)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -114,7 +114,7 @@ describe API::API, api: true do
expect(new_user.can_create_group).to eq(true)
end
- it "should create non-admin user" do
+ it "creates non-admin user" do
post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -124,7 +124,7 @@ describe API::API, api: true do
expect(new_user.can_create_group).to eq(false)
end
- it "should create non-admin users by default" do
+ it "creates non-admin users by default" do
post api('/users', admin), attributes_for(:user)
expect(response).to have_http_status(201)
user_id = json_response['id']
@@ -133,7 +133,7 @@ describe API::API, api: true do
expect(new_user.admin).to eq(false)
end
- it "should return 201 Created on success" do
+ it "returns 201 Created on success" do
post api("/users", admin), attributes_for(:user, projects_limit: 3)
expect(response).to have_http_status(201)
end
@@ -148,7 +148,7 @@ describe API::API, api: true do
expect(new_user.external).to be_falsy
end
- it 'should allow an external user to be created' do
+ it 'allows an external user to be created' do
post api("/users", admin), attributes_for(:user, external: true)
expect(response).to have_http_status(201)
@@ -158,7 +158,7 @@ describe API::API, api: true do
expect(new_user.external).to be_truthy
end
- it "should not create user with invalid email" do
+ it "does not create user with invalid email" do
post api('/users', admin),
email: 'invalid email',
password: 'password',
@@ -166,27 +166,27 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
end
- it 'should return 400 error if name not given' do
+ it 'returns 400 error if name not given' do
post api('/users', admin), attributes_for(:user).except(:name)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if password not given' do
+ it 'returns 400 error if password not given' do
post api('/users', admin), attributes_for(:user).except(:password)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if email not given' do
+ it 'returns 400 error if email not given' do
post api('/users', admin), attributes_for(:user).except(:email)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if username not given' do
+ it 'returns 400 error if username not given' do
post api('/users', admin), attributes_for(:user).except(:username)
expect(response).to have_http_status(400)
end
- it 'should return 400 error if user does not validate' do
+ it 'returns 400 error if user does not validate' do
post api('/users', admin),
password: 'pass',
email: 'test@example.com',
@@ -205,7 +205,7 @@ describe API::API, api: true do
to eq([Gitlab::Regex.namespace_regex_message])
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
post api("/users", user), attributes_for(:user)
expect(response).to have_http_status(403)
end
@@ -219,7 +219,7 @@ describe API::API, api: true do
name: 'foo'
end
- it 'should return 409 conflict error if user with same email exists' do
+ it 'returns 409 conflict error if user with same email exists' do
expect do
post api('/users', admin),
name: 'foo',
@@ -231,7 +231,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('Email has already been taken')
end
- it 'should return 409 conflict error if same username exists' do
+ it 'returns 409 conflict error if same username exists' do
expect do
post api('/users', admin),
name: 'foo',
@@ -246,7 +246,7 @@ describe API::API, api: true do
end
describe "GET /users/sign_up" do
- it "should redirect to sign in page" do
+ it "redirects to sign in page" do
get "/users/sign_up"
expect(response).to have_http_status(302)
expect(response).to redirect_to(new_user_session_path)
@@ -258,55 +258,55 @@ describe API::API, api: true do
before { admin }
- it "should update user with new bio" do
+ it "updates user with new bio" do
put api("/users/#{user.id}", admin), { bio: 'new test bio' }
expect(response).to have_http_status(200)
expect(json_response['bio']).to eq('new test bio')
expect(user.reload.bio).to eq('new test bio')
end
- it 'should update user with his own email' do
+ it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
expect(response).to have_http_status(200)
expect(json_response['email']).to eq(user.email)
expect(user.reload.email).to eq(user.email)
end
- it 'should update user with his own username' do
+ it 'updates user with his own username' do
put api("/users/#{user.id}", admin), username: user.username
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
expect(user.reload.username).to eq(user.username)
end
- it "should update user's existing identity" do
+ it "updates user's existing identity" do
put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
expect(response).to have_http_status(200)
expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
end
- it 'should update user with new identity' do
+ it 'updates user with new identity' do
put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
expect(response).to have_http_status(200)
expect(user.reload.identities.first.extern_uid).to eq('67890')
expect(user.reload.identities.first.provider).to eq('github')
end
- it "should update admin status" do
+ it "updates admin status" do
put api("/users/#{user.id}", admin), { admin: true }
expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
expect(user.reload.admin).to eq(true)
end
- it "should update external status" do
+ it "updates external status" do
put api("/users/#{user.id}", admin), { external: true }
expect(response.status).to eq 200
expect(json_response['external']).to eq(true)
expect(user.reload.external?).to be_truthy
end
- it "should not update admin status" do
+ it "does not update admin status" do
put api("/users/#{admin_user.id}", admin), { can_create_group: false }
expect(response).to have_http_status(200)
expect(json_response['is_admin']).to eq(true)
@@ -314,28 +314,28 @@ describe API::API, api: true do
expect(admin_user.can_create_group).to eq(false)
end
- it "should not allow invalid update" do
+ it "does not allow invalid update" do
put api("/users/#{user.id}", admin), { email: 'invalid email' }
expect(response).to have_http_status(400)
expect(user.reload.email).not_to eq('invalid email')
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
put api("/users/#{user.id}", user), attributes_for(:user)
expect(response).to have_http_status(403)
end
- it "should return 404 for non-existing user" 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')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
- it 'should return 400 error if user does not validate' do
+ it 'returns 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
password: 'pass',
email: 'test@example.com',
@@ -361,13 +361,13 @@ describe API::API, api: true do
@user = User.all.last
end
- it 'should return 409 conflict error if email address exists' do
+ it 'returns 409 conflict error if email address exists' do
put api("/users/#{@user.id}", admin), email: 'test@example.com'
expect(response).to have_http_status(409)
expect(@user.reload.email).to eq(@user.email)
end
- it 'should return 409 conflict error if username taken' do
+ it 'returns 409 conflict error if username taken' do
@user_id = User.all.last.id
put api("/users/#{@user.id}", admin), username: 'test'
expect(response).to have_http_status(409)
@@ -379,28 +379,28 @@ describe API::API, api: true do
describe "POST /users/:id/keys" do
before { admin }
- it "should not create invalid ssh key" 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')
end
- it 'should not create key without title' do
+ 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')
end
- it "should create ssh key" do
+ it "creates ssh key" do
key_attrs = attributes_for :key
expect do
post api("/users/#{user.id}/keys", admin), key_attrs
end.to change{ user.keys.count }.by(1)
end
- it "should return 405 for invalid ID" do
- post api("/users/ASDF/keys", admin)
- expect(response).to have_http_status(405)
+ it "returns 400 for invalid ID" do
+ post api("/users/999999/keys", admin)
+ expect(response).to have_http_status(400)
end
end
@@ -408,20 +408,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/users/#{user.id}/keys")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing user' do
+ it 'returns 404 for non-existing user' do
get api('/users/999999/keys', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return array of ssh keys' do
+ it 'returns array of ssh keys' do
user.keys << key
user.save
get api("/users/#{user.id}/keys", admin)
@@ -429,11 +429,6 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
-
- it "should return 405 for invalid ID" do
- get api("/users/ASDF/keys", admin)
- expect(response).to have_http_status(405)
- end
end
end
@@ -441,14 +436,14 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
delete api("/users/#{user.id}/keys/42")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should delete existing key' do
+ it 'deletes existing key' do
user.keys << key
user.save
expect do
@@ -457,7 +452,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it 'should return 404 error if user not found' do
+ it 'returns 404 error if user not found' do
user.keys << key
user.save
delete api("/users/999999/keys/#{key.id}", admin)
@@ -465,7 +460,7 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return 404 error if key not foud' do
+ it 'returns 404 error if key not foud' do
delete api("/users/#{user.id}/keys/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Key Not Found')
@@ -476,22 +471,22 @@ describe API::API, api: true do
describe "POST /users/:id/emails" do
before { admin }
- it "should not create invalid email" 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')
end
- it "should create email" do
+ it "creates email" do
email_attrs = attributes_for :email
expect do
post api("/users/#{user.id}/emails", admin), email_attrs
end.to change{ user.emails.count }.by(1)
end
- it "should raise error for invalid ID" do
- post api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(405)
+ it "raises error for invalid ID" do
+ post api("/users/999999/emails", admin)
+ expect(response).to have_http_status(400)
end
end
@@ -499,20 +494,20 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
get api("/users/#{user.id}/emails")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should return 404 for non-existing user' do
+ it 'returns 404 for non-existing user' do
get api('/users/999999/emails', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return array of emails' do
+ it 'returns array of emails' do
user.emails << email
user.save
get api("/users/#{user.id}/emails", admin)
@@ -521,7 +516,7 @@ describe API::API, api: true do
expect(json_response.first['email']).to eq(email.email)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
put api("/users/ASDF/emails", admin)
expect(response).to have_http_status(405)
end
@@ -532,14 +527,14 @@ describe API::API, api: true do
before { admin }
context 'when unauthenticated' do
- it 'should return authentication error' do
+ it 'returns authentication error' do
delete api("/users/#{user.id}/emails/42")
expect(response).to have_http_status(401)
end
end
context 'when authenticated' do
- it 'should delete existing email' do
+ it 'deletes existing email' do
user.emails << email
user.save
expect do
@@ -548,7 +543,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it 'should return 404 error if user not found' do
+ it 'returns 404 error if user not found' do
user.emails << email
user.save
delete api("/users/999999/emails/#{email.id}", admin)
@@ -556,51 +551,53 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it 'should return 404 error if email not foud' do
+ it 'returns 404 error if email not foud' do
delete api("/users/#{user.id}/emails/42", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
end
end
end
describe "DELETE /users/:id" do
+ let!(:namespace) { user.namespace }
before { admin }
- it "should delete user" do
+ it "deletes user" do
delete api("/users/#{user.id}", admin)
expect(response).to have_http_status(200)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
+ expect { Namespace.find(namespace.id) }.to raise_error ActiveRecord::RecordNotFound
expect(json_response['email']).to eq(user.email)
end
- it "should not delete for unauthenticated user" do
+ it "does not delete for unauthenticated user" do
delete api("/users/#{user.id}")
expect(response).to have_http_status(401)
end
- it "shouldn't available for non admin users" do
+ it "is not available for non admin users" do
delete api("/users/#{user.id}", user)
expect(response).to have_http_status(403)
end
- it "should return 404 for non-existing user" do
+ it "returns 404 for non-existing user" do
delete api("/users/999999", admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe "GET /user" do
- it "should return current 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)
@@ -610,7 +607,7 @@ describe API::API, api: true do
expect(json_response['projects_limit']).to eq(user.projects_limit)
end
- it "should return 401 error if user is unauthenticated" do
+ it "returns 401 error if user is unauthenticated" do
get api("/user")
expect(response).to have_http_status(401)
end
@@ -618,14 +615,14 @@ describe API::API, api: true do
describe "GET /user/keys" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/user/keys")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return array of ssh keys" do
+ it "returns array of ssh keys" do
user.keys << key
user.save
get api("/user/keys", user)
@@ -637,7 +634,7 @@ describe API::API, api: true do
end
describe "GET /user/keys/:id" do
- it "should return single key" do
+ it "returns single key" do
user.keys << key
user.save
get api("/user/keys/#{key.id}", user)
@@ -645,13 +642,13 @@ describe API::API, api: true do
expect(json_response["title"]).to eq(key.title)
end
- it "should return 404 Not Found within invalid ID" do
+ it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 error if admin accesses user's ssh key" do
+ it "returns 404 error if admin accesses user's ssh key" do
user.keys << key
user.save
admin
@@ -660,14 +657,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 for invalid ID" do
+ it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /user/keys" do
- it "should create ssh key" do
+ it "creates ssh key" do
key_attrs = attributes_for :key
expect do
post api("/user/keys", user), key_attrs
@@ -675,31 +672,31 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it "should return a 401 error if unauthorized" do
+ it "returns a 401 error if unauthorized" do
post api("/user/keys"), title: 'some title', key: 'some key'
expect(response).to have_http_status(401)
end
- it "should not create ssh key without key" 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')
end
- it 'should not create ssh key without title' do
+ 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')
end
- it "should not create ssh key without title" do
+ it "does not create ssh key without title" do
post api("/user/keys", user), key: "somekey"
expect(response).to have_http_status(400)
end
end
describe "DELETE /user/keys/:id" do
- it "should delete existed key" do
+ it "deletes existed key" do
user.keys << key
user.save
expect do
@@ -708,33 +705,33 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "should return success if key ID not found" do
+ it "returns success if key ID not found" do
delete api("/user/keys/42", user)
expect(response).to have_http_status(200)
end
- it "should return 401 error if unauthorized" do
+ it "returns 401 error if unauthorized" do
user.keys << key
user.save
delete api("/user/keys/#{key.id}")
expect(response).to have_http_status(401)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe "GET /user/emails" do
context "when unauthenticated" do
- it "should return authentication error" do
+ it "returns authentication error" do
get api("/user/emails")
expect(response).to have_http_status(401)
end
end
context "when authenticated" do
- it "should return array of emails" do
+ it "returns array of emails" do
user.emails << email
user.save
get api("/user/emails", user)
@@ -746,7 +743,7 @@ describe API::API, api: true do
end
describe "GET /user/emails/:id" do
- it "should return single email" do
+ it "returns single email" do
user.emails << email
user.save
get api("/user/emails/#{email.id}", user)
@@ -754,13 +751,13 @@ describe API::API, api: true do
expect(json_response["email"]).to eq(email.email)
end
- it "should return 404 Not Found within invalid ID" 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')
end
- it "should return 404 error if admin accesses user's email" do
+ it "returns 404 error if admin accesses user's email" do
user.emails << email
user.save
admin
@@ -769,14 +766,14 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "should return 404 for invalid ID" do
+ it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /user/emails" do
- it "should create email" do
+ it "creates email" do
email_attrs = attributes_for :email
expect do
post api("/user/emails", user), email_attrs
@@ -784,12 +781,12 @@ describe API::API, api: true do
expect(response).to have_http_status(201)
end
- it "should return a 401 error if unauthorized" do
+ it "returns a 401 error if unauthorized" do
post api("/user/emails"), email: 'some email'
expect(response).to have_http_status(401)
end
- it "should not create email with invalid email" 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')
@@ -797,7 +794,7 @@ describe API::API, api: true do
end
describe "DELETE /user/emails/:id" do
- it "should delete existed email" do
+ it "deletes existed email" do
user.emails << email
user.save
expect do
@@ -806,44 +803,44 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
end
- it "should return success if email ID not found" do
+ it "returns success if email ID not found" do
delete api("/user/emails/42", user)
expect(response).to have_http_status(200)
end
- it "should return 401 error if unauthorized" do
+ it "returns 401 error if unauthorized" do
user.emails << email
user.save
delete api("/user/emails/#{email.id}")
expect(response).to have_http_status(401)
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
end
end
describe 'PUT /user/:id/block' do
before { admin }
- it 'should block existing user' do
+ it 'blocks existing user' do
put api("/users/#{user.id}/block", admin)
expect(response).to have_http_status(200)
expect(user.reload.state).to eq('blocked')
end
- it 'should not re-block ldap blocked users' do
+ it 'does not re-block ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/block", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
- it 'should not be available for non admin users' do
+ it 'does not be available for non admin users' do
put api("/users/#{user.id}/block", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
- it 'should return a 404 error if user id not found' do
+ it 'returns a 404 error if user id not found' do
put api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -854,37 +851,37 @@ describe API::API, api: true do
let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
- it 'should unblock existing user' do
+ it 'unblocks existing user' do
put api("/users/#{user.id}/unblock", admin)
expect(response).to have_http_status(200)
expect(user.reload.state).to eq('active')
end
- it 'should unblock a blocked user' do
+ it 'unblocks a blocked user' do
put api("/users/#{blocked_user.id}/unblock", admin)
expect(response).to have_http_status(200)
expect(blocked_user.reload.state).to eq('active')
end
- it 'should not unblock ldap blocked users' do
+ it 'does not unblock ldap blocked users' do
put api("/users/#{ldap_blocked_user.id}/unblock", admin)
expect(response).to have_http_status(403)
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
- it 'should not be available for non admin users' do
+ it 'does not be available for non admin users' do
put api("/users/#{user.id}/unblock", user)
expect(response).to have_http_status(403)
expect(user.reload.state).to eq('active')
end
- it 'should return a 404 error if user id not found' do
+ it 'returns a 404 error if user id not found' do
put api('/users/9999/block', admin)
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
- it "should raise error for invalid ID" do
+ it "raises error for invalid ID" do
expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
end
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index ddba18245f8..05fbdb909dc 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -12,7 +12,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/variables' do
context 'authorized user with proper permissions' do
- it 'should return project variables' do
+ it 'returns project variables' do
get api("/projects/#{project.id}/variables", user)
expect(response).to have_http_status(200)
@@ -21,7 +21,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not return project variables' do
+ it 'does not return project variables' do
get api("/projects/#{project.id}/variables", user2)
expect(response).to have_http_status(403)
@@ -29,7 +29,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not return project variables' do
+ it 'does not return project variables' do
get api("/projects/#{project.id}/variables")
expect(response).to have_http_status(401)
@@ -39,14 +39,14 @@ describe API::API, api: true do
describe 'GET /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should return project variable details' do
+ it 'returns project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user)
expect(response).to have_http_status(200)
expect(json_response['value']).to eq(variable.value)
end
- it 'should respond with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
get api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -54,7 +54,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not return project variable details' do
+ it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -62,7 +62,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not return project variable details' do
+ it 'does not return project variable details' do
get api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
@@ -72,7 +72,7 @@ describe API::API, api: true do
describe 'POST /projects/:id/variables' do
context 'authorized user with proper permissions' do
- it 'should create variable' do
+ it 'creates variable' do
expect do
post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
end.to change{project.variables.count}.by(1)
@@ -82,7 +82,7 @@ describe API::API, api: true do
expect(json_response['value']).to eq('VALUE_2')
end
- it 'should not allow to duplicate variable key' do
+ it 'does not allow to duplicate variable key' do
expect do
post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
end.to change{project.variables.count}.by(0)
@@ -92,7 +92,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not create variable' do
+ it 'does not create variable' do
post api("/projects/#{project.id}/variables", user2)
expect(response).to have_http_status(403)
@@ -100,7 +100,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not create variable' do
+ it 'does not create variable' do
post api("/projects/#{project.id}/variables")
expect(response).to have_http_status(401)
@@ -110,7 +110,7 @@ describe API::API, api: true do
describe 'PUT /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should update variable data' do
+ it 'updates variable data' do
initial_variable = project.variables.first
value_before = initial_variable.value
@@ -123,7 +123,7 @@ describe API::API, api: true do
expect(updated_variable.value).to eq('VALUE_1_UP')
end
- it 'should responde with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
put api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -131,7 +131,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not update variable' do
+ it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -139,7 +139,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not update variable' do
+ it 'does not update variable' do
put api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
@@ -149,14 +149,14 @@ describe API::API, api: true do
describe 'DELETE /projects/:id/variables/:key' do
context 'authorized user with proper permissions' do
- it 'should delete variable' do
+ it 'deletes variable' do
expect do
delete api("/projects/#{project.id}/variables/#{variable.key}", user)
end.to change{project.variables.count}.by(-1)
expect(response).to have_http_status(200)
end
- it 'should responde with 404 Not Found if requesting non-existing variable' do
+ it 'responds with 404 Not Found if requesting non-existing variable' do
delete api("/projects/#{project.id}/variables/non_existing_variable", user)
expect(response).to have_http_status(404)
@@ -164,7 +164,7 @@ describe API::API, api: true do
end
context 'authorized user with invalid permissions' do
- it 'should not delete variable' do
+ it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
expect(response).to have_http_status(403)
@@ -172,7 +172,7 @@ describe API::API, api: true do
end
context 'unauthorized user' do
- it 'should not delete variable' do
+ it 'does not delete variable' do
delete api("/projects/#{project.id}/variables/#{variable.key}")
expect(response).to have_http_status(401)
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index e7cbc3dd3a7..ca7932dc5da 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -6,112 +6,102 @@ describe Ci::API::API do
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
let(:project) { FactoryGirl.create(:empty_project) }
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
describe "Builds API for runners" do
- let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
- let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, project: project, ref: 'master') }
before do
- FactoryGirl.create :ci_runner_project, project: project, runner: runner
+ project.runners << runner
end
describe "POST /builds/register" do
- it "should start a build" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil)
- build = pipeline.builds.first
+ let!(:build) { create(:ci_build, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "starts a build" do
+ register_builds info: { platform: :darwin }
expect(response).to have_http_status(201)
expect(json_response['sha']).to eq(build.sha)
expect(runner.reload.platform).to eq("darwin")
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true }
+ )
end
- it "should return 404 error if no pending build found" do
- post ci_api("/builds/register"), token: runner.token
-
- expect(response).to have_http_status(404)
- end
-
- it "should return 404 error if no builds for specific runner" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: shared_project)
- FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+ context 'when builds are finished' do
+ before do
+ build.success
+ end
- post ci_api("/builds/register"), token: runner.token
+ it "returns 404 error if no builds for specific runner" do
+ register_builds
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- it "should return 404 error if no builds for shared runner" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project)
- FactoryGirl.create(:ci_build, pipeline: pipeline, status: 'pending')
+ context 'for other project with builds' do
+ before do
+ build.success
+ create(:ci_build, :pending)
+ end
- post ci_api("/builds/register"), token: shared_runner.token
+ it "returns 404 error if no builds for shared runner" do
+ register_builds
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- it "returns options" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil)
+ context 'for shared runner' do
+ let(:shared_runner) { create(:ci_runner, token: "SharedRunner") }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "should return 404 error if no builds for shared runner" do
+ register_builds shared_runner.token
- expect(response).to have_http_status(201)
- expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(response).to have_http_status(404)
+ end
end
- it "returns variables" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil)
- project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ context 'for triggered build' do
+ before do
+ trigger = create(:ci_trigger, project: project)
+ create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [build], trigger: trigger)
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+ end
- expect(response).to have_http_status(201)
- expect(json_response["variables"]).to eq([
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
- ])
+ it "returns variables for triggers" do
+ register_builds info: { platform: :darwin }
+
+ expect(response).to have_http_status(201)
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
+ )
+ end
end
- it "returns variables for triggers" do
- trigger = FactoryGirl.create(:ci_trigger, project: project)
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
-
- trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger)
- pipeline.create_builds(nil, trigger_request)
- project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
-
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response["variables"]).to eq([
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
- { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
- ])
- end
+ context 'with multiple builds' do
+ before do
+ build.success
+ end
- it "returns dependent builds" do
- pipeline = FactoryGirl.create(:ci_pipeline, project: project, ref: 'master')
- pipeline.create_builds(nil, nil)
- pipeline.builds.where(stage: 'test').each(&:success)
+ let!(:test_build) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
- post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+ it "returns dependent builds" do
+ register_builds info: { platform: :darwin }
- expect(response).to have_http_status(201)
- expect(json_response["depends_on_builds"].count).to eq(2)
- expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec")
+ expect(response).to have_http_status(201)
+ expect(json_response["id"]).to eq(test_build.id)
+ expect(json_response["depends_on_builds"].count).to eq(1)
+ expect(json_response["depends_on_builds"][0]).to include('id' => build.id, 'name' => 'spinach')
+ end
end
%w(name version revision platform architecture).each do |param|
@@ -121,8 +111,9 @@ describe Ci::API::API do
subject { runner.read_attribute(param.to_sym) }
it do
- post ci_api("/builds/register"), token: runner.token, info: { param => value }
- expect(response).to have_http_status(404)
+ register_builds info: { param => value }
+
+ expect(response).to have_http_status(201)
runner.reload
is_expected.to eq(value)
end
@@ -131,8 +122,7 @@ describe Ci::API::API do
context 'when build has no tags' do
before do
- pipeline = create(:ci_pipeline, project: project)
- create(:ci_build, pipeline: pipeline, tags: [])
+ build.update(tags: [])
end
context 'when runner is allowed to pick untagged builds' do
@@ -154,42 +144,40 @@ describe Ci::API::API do
expect(response).to have_http_status 404
end
end
+ end
- def register_builds
- post ci_api("/builds/register"), token: runner.token,
- info: { platform: :darwin }
- end
+ def register_builds(token = runner.token, **params)
+ post ci_api("/builds/register"), params.merge(token: token)
end
end
describe "PUT /builds/:id" do
- let(:pipeline) {create(:ci_pipeline, project: project)}
- let(:build) { create(:ci_build, :trace, pipeline: pipeline, runner_id: runner.id) }
+ let(:build) { create(:ci_build, :pending, :trace, pipeline: pipeline, runner_id: runner.id) }
before do
build.run!
put ci_api("/builds/#{build.id}"), token: runner.token
end
- it "should update a running build" do
+ it "updates a running build" do
expect(response).to have_http_status(200)
end
- it 'should not override trace information when no trace is given' do
+ it 'does not override trace information when no trace is given' do
expect(build.reload.trace).to eq 'BUILD TRACE'
end
context 'build has been erased' do
let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
end
describe 'PATCH /builds/:id/trace.txt' do
- let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+ 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' }) }
@@ -237,8 +225,7 @@ describe Ci::API::API do
context "Artifacts" do
let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let(:build) { create(:ci_build, pipeline: pipeline, runner_id: runner.id) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline, runner_id: runner.id) }
let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") }
let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
@@ -280,7 +267,7 @@ describe Ci::API::API do
context 'authorization token is invalid' do
before { post authorize_url, { token: 'invalid', filesize: 100 } }
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response).to have_http_status(403)
end
end
@@ -300,7 +287,7 @@ describe Ci::API::API do
upload_artifacts(file_upload, headers_with_token)
end
- it 'should respond with forbidden' do
+ it 'responds with forbidden' do
expect(response.status).to eq 403
end
end
@@ -342,7 +329,7 @@ describe Ci::API::API do
end
end
- context 'should post artifacts file and metadata file' do
+ context 'posts artifacts file and metadata file' do
let!(:artifacts) { file_upload }
let!(:metadata) { file_upload2 }
@@ -354,7 +341,7 @@ describe Ci::API::API do
post(post_url, post_data, headers_with_token)
end
- context 'post data accelerated by workhorse is correct' do
+ context 'posts data accelerated by workhorse is correct' do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
@@ -422,7 +409,7 @@ describe Ci::API::API do
end
context "artifacts file is too large" do
- it "should fail to post too large artifact" do
+ it "fails to post too large artifact" do
stub_application_setting(max_artifacts_size: 0)
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_http_status(413)
@@ -430,14 +417,14 @@ describe Ci::API::API do
end
context "artifacts post request does not contain file" do
- it "should fail to post artifacts without file" do
+ it "fails to post artifacts without file" do
post post_url, {}, headers_with_token
expect(response).to have_http_status(400)
end
end
context 'GitLab Workhorse is not configured' do
- it "should fail to post artifacts without GitLab-Workhorse" do
+ it "fails to post artifacts without GitLab-Workhorse" do
post post_url, { token: build.token }, {}
expect(response).to have_http_status(403)
end
@@ -456,7 +443,7 @@ describe Ci::API::API do
FileUtils.remove_entry @tmpdir
end
- it "should fail to post artifacts for outside of tmp path" do
+ it "fails to post artifacts for outside of tmp path" do
upload_artifacts(file_upload, headers_with_token)
expect(response).to have_http_status(400)
end
@@ -482,7 +469,7 @@ describe Ci::API::API do
build.reload
end
- it 'should remove build artifacts' do
+ it 'removes build artifacts' do
expect(response).to have_http_status(200)
expect(build.artifacts_file.exists?).to be_falsy
expect(build.artifacts_metadata.exists?).to be_falsy
@@ -500,14 +487,14 @@ describe Ci::API::API do
'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
end
- it 'should download artifact' do
+ it 'downloads artifact' do
expect(response).to have_http_status(200)
expect(response.headers).to include download_headers
end
end
context 'build does not has artifacts' do
- it 'should respond with not found' do
+ it 'responds with not found' do
expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index f12678e5a8e..0a0f979f57d 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -19,17 +19,17 @@ describe Ci::API::API do
end
context 'Handles errors' do
- it 'should return bad request if token is missing' do
+ it 'returns bad request if token is missing' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
expect(response).to have_http_status(400)
end
- it 'should return not found if project is not found' do
+ it 'returns not found if project is not found' do
post ci_api('/projects/0/refs/master/trigger'), options
expect(response).to have_http_status(404)
end
- it 'should return unauthorized if token is for different project' do
+ it 'returns unauthorized if token is for different project' do
post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
expect(response).to have_http_status(401)
end
@@ -38,14 +38,15 @@ describe Ci::API::API do
context 'Have a commit' do
let(:pipeline) { project.pipelines.last }
- it 'should create builds' do
+ it 'creates builds' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options
expect(response).to have_http_status(201)
pipeline.builds.reload
- expect(pipeline.builds.size).to eq(2)
+ expect(pipeline.builds.pending.size).to eq(2)
+ expect(pipeline.builds.size).to eq(5)
end
- it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ it 'returns bad request with no builds created if there\'s no commit for that ref' do
post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('No builds created')
@@ -56,19 +57,19 @@ describe Ci::API::API do
{ 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
end
- it 'should validate variables to be a hash' do
+ it 'validates variables to be a hash' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a hash')
end
- it 'should validate variables needs to be a map of key-valued strings' do
+ it 'validates variables needs to be a map of key-valued strings' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
expect(response).to have_http_status(400)
expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
end
- it 'create trigger request with variables' do
+ it 'creates trigger request with variables' do
post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: variables)
expect(response).to have_http_status(201)
pipeline.builds.reload
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 82ab582beac..afaf4b7cefb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -75,9 +75,9 @@ describe 'Git HTTP requests', lib: true do
context "with correct credentials" do
let(:env) { { user: user.username, password: user.password } }
- it "uploads get status 200 (because Git hooks do the real check)" do
+ it "uploads get status 403" do
upload(path, env) do |response|
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(403)
end
end
@@ -86,7 +86,7 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
upload(path, env) do |response|
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(403)
end
end
end
@@ -198,6 +198,45 @@ describe 'Git HTTP requests', lib: true do
end
end
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+ let(:access_token) { create(:personal_access_token, user: user) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'when username and password are provided' do
+ it 'rejects the clone attempt' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+
+ it 'rejects the push attempt' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
+ end
+ end
+
+ context 'when username and personal access token are provided' do
+ it 'allows clones' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+
+ it 'allows pushes' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
@@ -236,9 +275,9 @@ describe 'Git HTTP requests', lib: true do
end
end
- it "uploads get status 200 (because Git hooks do the real check)" do
+ it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(200)
+ expect(response).to have_http_status(404)
end
end
end
@@ -349,19 +388,19 @@ describe 'Git HTTP requests', lib: true do
end
end
- def clone_get(project, options={})
+ def clone_get(project, options = {})
get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def clone_post(project, options={})
+ def clone_post(project, options = {})
post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_get(project, options={})
+ def push_get(project, options = {})
get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
- def push_post(project, options={})
+ def push_post(project, options = {})
post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
new file mode 100644
index 00000000000..4c9b4a8ba42
--- /dev/null
+++ b/spec/requests/lfs_http_spec.rb
@@ -0,0 +1,797 @@
+require 'spec_helper'
+
+describe 'Git LFS API and storage' do
+ let(:user) { create(:user) }
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ let(:headers) do
+ {
+ 'Authorization' => authorization,
+ 'X-Sendfile-Type' => sendfile
+ }.compact
+ end
+ let(:authorization) { }
+ let(:sendfile) { }
+
+ let(:sample_oid) { lfs_object.oid }
+ let(:sample_size) { lfs_object.size }
+
+ describe 'when lfs is disabled' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }
+ end
+ let(:authorization) { authorize_user }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ end
+
+ it 'responds with 501' do
+ expect(response).to have_http_status(501)
+ expect(json_response).to include('message' => 'Git LFS is not enabled on this GitLab server, contact your admin.')
+ end
+ end
+
+ describe 'deprecated API' do
+ let(:project) { create(:empty_project) }
+
+ before do
+ enable_lfs
+ end
+
+ shared_examples 'a deprecated' do
+ it 'responds with 501' do
+ expect(response).to have_http_status(501)
+ end
+
+ it 'returns deprecated message' do
+ expect(json_response).to include('message' => 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.')
+ end
+ end
+
+ context 'when fetching lfs object using deprecated API' do
+ let(:authorization) { authorize_user }
+
+ before do
+ get "#{project.http_url_to_repo}/info/lfs/objects/#{sample_oid}", nil, headers
+ end
+
+ it_behaves_like 'a deprecated'
+ end
+
+ context 'when handling lfs request using deprecated API' do
+ let(:authorization) { authorize_user }
+ before do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects", nil, headers
+ end
+
+ it_behaves_like 'a deprecated'
+ end
+ end
+
+ describe 'when fetching lfs object' do
+ let(:project) { create(:empty_project) }
+ let(:update_permissions) { }
+
+ before do
+ enable_lfs
+ update_permissions
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+ end
+
+ context 'and request comes from gitlab-workhorse' do
+ context 'without user being authorized' do
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'with required headers' do
+ shared_examples 'responds with a file' do
+ let(:sendfile) { 'X-Sendfile' }
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with the file location' do
+ expect(response.headers['Content-Type']).to eq('application/octet-stream')
+ expect(response.headers['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
+
+ context 'with user is authorized' do
+ let(:authorization) { authorize_user }
+
+ context 'and does not have project access' do
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'and does have project access' do
+ let(:update_permissions) do
+ project.team << [user, :master]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+ end
+
+ context 'without required headers' do
+ let(:authorization) { authorize_user }
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+ end
+
+ describe 'when handling lfs batch request' do
+ let(:update_lfs_permissions) { }
+ let(:update_user_permissions) { }
+
+ before do
+ enable_lfs
+ update_lfs_permissions
+ update_user_permissions
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+ end
+
+ describe 'download' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ shared_examples 'an authorized requests' do
+ context 'when downloading an lfs object that is assigned to our project' do
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
+ }
+ }])
+ end
+ end
+
+ context 'when downloading an lfs object that is assigned to other project' do
+ let(:other_project) { create(:empty_project) }
+ let(:update_lfs_permissions) do
+ other_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading a lfs object that does not exist' do
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with an 404 for specific object' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
+ end
+ end
+
+ context 'when downloading one new and one existing lfs object' do
+ let(:body) do
+ { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }
+ end
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link for the new object' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => authorization }
+ }
+ }
+ }])
+ end
+ end
+ end
+
+ context 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ let(:update_user_permissions) do
+ project.team << [user, role]
+ end
+
+ it_behaves_like 'an authorized requests' do
+ let(:role) { :reporter }
+ end
+
+ context 'when user does is not member of the project' do
+ let(:update_user_permissions) { nil }
+
+ it 'responds with 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user does not have download access' do
+ let(:role) { :guest }
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'an authorized requests'
+ end
+
+ context 'when user is not authenticated' do
+ describe 'is accessing public project' do
+ let(:project) { create(:project, :public) }
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and href to download' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with status 200 and href to download' do
+ expect(json_response).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => {}
+ }
+ }
+ }])
+ end
+ end
+
+ describe 'is accessing non-public project' do
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with authorization required' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+ end
+
+ describe 'upload' do
+ let(:project) { create(:project, :public) }
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ describe 'when request is authenticated' do
+ describe 'when user has project push access' do
+ let(:authorization) { authorize_user }
+
+ let(:update_user_permissions) do
+ project.team << [user, :developer]
+ end
+
+ context 'when pushing an lfs object that already exists' do
+ let(:other_project) { create(:empty_project) }
+ let(:update_lfs_permissions) do
+ other_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with links the object to the project' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq(sample_oid)
+ expect(json_response['objects'].first['size']).to eq(sample_size)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(other_project.id)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
+ context 'when pushing a lfs object that does not exist' do
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link' do
+ expect(json_response['objects']).to be_kind_of(Array)
+ expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq('Authorization' => authorization)
+ end
+ end
+
+ context 'when pushing one new and one existing lfs object' do
+ let(:body) do
+ { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }
+ end
+
+ let(:update_lfs_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with upload hypermedia link for the new object' do
+ expect(json_response['objects']).to be_kind_of(Array)
+
+ expect(json_response['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ expect(json_response['objects'].first['actions']['upload']['href']).to eq("#{project.http_url_to_repo}/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(json_response['objects'].first['actions']['upload']['header']).to eq("Authorization" => authorization)
+
+ expect(json_response['objects'].last['oid']).to eq(sample_oid)
+ expect(json_response['objects'].last['size']).to eq(sample_size)
+ expect(json_response['objects'].last).not_to have_key('actions')
+ end
+ end
+ end
+
+ context 'when user does not have push access' do
+ let(:authorization) { authorize_user }
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it 'responds with 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ context 'when user has push access' do
+ let(:update_user_permissions) do
+ project.team << [user, :master]
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'when user does not have push access' do
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ context 'when CI is authorized' do
+ let(:authorization) { authorize_ci_project }
+
+ it 'responds with status 403' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ describe 'unsupported' do
+ let(:project) { create(:empty_project) }
+ let(:authorization) { authorize_user }
+ let(:body) do
+ { 'operation' => 'other',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ describe 'when pushing a lfs object' do
+ before do
+ enable_lfs
+ end
+
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 401' do
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with 403' do
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ put_finalize('/etc/passwd')
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'to one project' do
+ let(:project) { create(:empty_project) }
+
+ describe 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'lfs object is linked to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
+
+ context 'invalid tempfiles' do
+ it 'rejects slashes in the tempfile name (path traversal' do
+ put_finalize('foo/bar')
+ expect(response).to have_http_status(403)
+ end
+
+ it 'rejects tempfile names that do not start with the oid' do
+ put_finalize("foo#{sample_oid}")
+ expect(response).to have_http_status(403)
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ before do
+ project.team << [user, :reporter]
+ end
+
+ it_behaves_like 'forbidden'
+ end
+ end
+
+ context 'when CI is authenticated' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'for unauthenticated' do
+ it_behaves_like 'unauthorized'
+ end
+ end
+
+ describe 'to a forked project' do
+ let(:upstream_project) { create(:project, :public) }
+ let(:project_owner) { create(:user) }
+ let(:project) { fork_project(upstream_project, project_owner) }
+
+ describe 'when user is authenticated' do
+ let(:authorization) { authorize_user }
+
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ put_authorize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'with location of lfs store and object details' do
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ put_finalize
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'lfs object is linked to the source project' do
+ expect(lfs_object.projects.pluck(:id)).to include(upstream_project.id)
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ it_behaves_like 'forbidden'
+ end
+ end
+
+ context 'when CI is authenticated' do
+ let(:authorization) { authorize_ci_project }
+
+ it_behaves_like 'unauthorized'
+ end
+
+ context 'for unauthenticated' do
+ it_behaves_like 'unauthorized'
+ end
+
+ describe 'and second project not related to fork or a source project' do
+ let(:second_project) { create(:empty_project) }
+ let(:authorization) { authorize_user }
+
+ before do
+ second_project.team << [user, :master]
+ upstream_project.lfs_objects << lfs_object
+ end
+
+ context 'when pushing the same lfs object to the second project' do
+ before do
+ put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
+ headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file).compact
+ end
+
+ it 'responds with status 200' do
+ expect(response).to have_http_status(200)
+ end
+
+ it 'links the lfs object to the project' do
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
+ end
+ end
+ end
+ end
+
+ def put_authorize
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
+ end
+
+ def put_finalize(lfs_tmp = lfs_tmp_file)
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", nil,
+ headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp).compact
+ end
+
+ def lfs_tmp_file
+ "#{sample_oid}012345678"
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ def authorize_ci_project
+ ActionController::HttpAuthentication::Basic.encode_credentials('gitlab-ci-token', project.runners_token)
+ end
+
+ def authorize_user
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def fork_project(project, user, object = nil)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ Projects::ForkService.new(project, user, {}).execute
+ end
+
+ def post_lfs_json(url, body = nil, headers = nil)
+ post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json'))
+ end
+
+ def json_response
+ @json_response ||= JSON.parse(response.body)
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 8b19936ae6d..69eeb45ed71 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -1,6 +1,5 @@
require 'spec_helper'
-# team_update_admin_user PUT /admin/users/:id/team_update(.:format) admin/users#team_update
# block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block
# unblock_admin_user PUT /admin/users/:id/unblock(.:format) admin/users#unblock
# admin_users GET /admin/users(.:format) admin/users#index
@@ -11,10 +10,6 @@ require 'spec_helper'
# PUT /admin/users/:id(.:format) admin/users#update
# DELETE /admin/users/:id(.:format) admin/users#destroy
describe Admin::UsersController, "routing" do
- it "to #team_update" do
- expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1')
- end
-
it "to #block" do
expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1')
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 620f328a114..77842057a10 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -60,7 +60,7 @@ end
# project GET /:id(.:format) projects#show
# PUT /:id(.:format) projects#update
# DELETE /:id(.:format) projects#destroy
-# markdown_preview_project POST /:id/markdown_preview(.:format) projects#markdown_preview
+# 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')
@@ -91,9 +91,9 @@ describe ProjectsController, 'routing' do
expect(delete('/gitlab/gitlabhq')).to route_to('projects#destroy', namespace_id: 'gitlab', id: 'gitlabhq')
end
- it 'to #markdown_preview' do
- expect(post('/gitlab/gitlabhq/markdown_preview')).to(
- route_to('projects#markdown_preview', namespace_id: 'gitlab', id: 'gitlabhq')
+ it 'to #preview_markdown' do
+ expect(post('/gitlab/gitlabhq/preview_markdown')).to(
+ route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq')
)
end
end
@@ -135,10 +135,6 @@ describe Projects::RepositoriesController, 'routing' do
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
-
- it 'to #show' do
- expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq')
- end
end
describe Projects::BranchesController, 'routing' do
@@ -483,13 +479,16 @@ 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/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ 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
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 8a8e131c57b..4bc3cddd9c2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -98,7 +98,7 @@ describe SnippetsController, "routing" do
end
# help GET /help(.:format) help#index
-# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/}
+# help_page GET /help/*path(.:format) help#show
# help_shortcuts GET /help/shortcuts(.:format) help#shortcuts
# help_ui GET /help/ui(.:format) help#ui
describe HelpController, "routing" do
@@ -107,25 +107,25 @@ describe HelpController, "routing" do
end
it 'to #show' do
- path = '/help/markdown/markdown.md'
+ path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show',
- category: 'markdown',
- file: 'markdown',
+ path: 'user/markdown',
format: 'md')
path = '/help/workflow/protected_branches/protected_branches1.png'
expect(get(path)).to route_to('help#show',
- category: 'workflow/protected_branches',
- file: 'protected_branches1',
+ path: 'workflow/protected_branches/protected_branches1',
format: 'png')
- end
- it 'to #shortcuts' do
- expect(get('/help/shortcuts')).to route_to('help#shortcuts')
+ path = '/help/ui'
+ expect(get(path)).to route_to('help#ui')
end
+end
- it 'to #ui' do
- expect(get('/help/ui')).to route_to('help#ui')
+# koding GET /koding(.:format) koding#index
+describe KodingController, "routing" do
+ it "to #index" do
+ expect(get("/koding")).to route_to('koding#index')
end
end
@@ -183,18 +183,10 @@ describe Profiles::KeysController, "routing" do
expect(post("/profile/keys")).to route_to('profiles/keys#create')
end
- it "to #edit" do
- expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1')
- end
-
it "to #show" do
expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1')
end
- it "to #update" do
- expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1')
- end
-
it "to #destroy" do
expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1')
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 67777ad48bc..7cc71f706ce 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -87,51 +87,105 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do
end
context 'user authorization' do
- let(:project) { create(:project) }
let(:current_user) { create(:user) }
- context 'allow to use scope-less authentication' do
- it_behaves_like 'a valid token'
- end
+ context 'for private project' do
+ let(:project) { create(:empty_project) }
- context 'allow developer to push images' do
- before { project.team << [current_user, :developer] }
+ context 'allow to use scope-less authentication' do
+ it_behaves_like 'a valid token'
+ end
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:push" }
+ context 'allow developer to push images' do
+ before { project.team << [current_user, :developer] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'a pushable'
end
- it_behaves_like 'a pushable'
- end
+ context 'allow reporter to pull images' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
- context 'allow reporter to pull images' do
- before { project.team << [current_user, :reporter] }
+ it_behaves_like 'a pullable'
+ end
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull" }
+ context 'return a least of privileges' do
+ before { project.team << [current_user, :reporter] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push,pull" }
+ end
+
+ it_behaves_like 'a pullable'
end
- it_behaves_like 'a pullable'
+ context 'disallow guest to pull or push images' do
+ before { project.team << [current_user, :guest] }
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- context 'return a least of privileges' do
- before { project.team << [current_user, :reporter] }
+ context 'for public project' do
+ let(:project) { create(:empty_project, :public) }
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:push,pull" }
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a pullable'
end
- it_behaves_like 'a pullable'
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- context 'disallow guest to pull or push images' do
- before { project.team << [current_user, :guest] }
+ context 'for internal project' do
+ let(:project) { create(:empty_project, :internal) }
- let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ context 'for internal user' do
+ context 'allow anyone to pull images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull" }
+ end
+
+ it_behaves_like 'a pullable'
+ end
+
+ context 'disallow anyone to push images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
- it_behaves_like 'an inaccessible'
+ context 'for external user' do
+ let(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ end
end
end
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
new file mode 100644
index 00000000000..a1a4dd4c57c
--- /dev/null
+++ b/spec/services/boards/create_service_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Boards::CreateService, services: true do
+ describe '#execute' do
+ subject(:service) { described_class.new(project, double) }
+
+ context 'when project does not have a board' do
+ let(:project) { create(:empty_project, board: nil) }
+
+ it 'creates a new board' do
+ expect { service.execute }.to change(Board, :count).by(1)
+ end
+
+ it 'creates default lists' do
+ service.execute
+
+ expect(project.board.lists.size).to eq 2
+ expect(project.board.lists.first).to be_backlog
+ expect(project.board.lists.last).to be_done
+ end
+ end
+
+ context 'when project has a board' do
+ let!(:project) { create(:project_with_board) }
+
+ it 'does not create a new board' do
+ expect { service.execute }.not_to change(Board, :count)
+ end
+
+ it 'does not create board lists' do
+ expect { service.execute }.not_to change(project.board.lists, :count)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
new file mode 100644
index 00000000000..cf4c5f13635
--- /dev/null
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Boards::Issues::ListService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+ let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+ let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+ let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
+ let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+ let!(:reopened_issue1) { create(:issue, :reopened, project: project) }
+
+ let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) }
+ let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) }
+ let!(:list2_issue1) { create(:labeled_issue, project: project, labels: [testing]) }
+
+ let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
+ let!(:closed_issue3) { create(:issue, :closed, project: project) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates search to IssuesFinder' do
+ params = { id: list1.id }
+
+ expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
+
+ described_class.new(project, user, params).execute
+ end
+
+ context 'sets default order to priority' do
+ it 'returns opened issues when listing issues from Backlog' do
+ params = { id: backlog.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
+ end
+
+ it 'returns closed issues when listing issues from Done' do
+ params = { id: done.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
+ end
+
+ it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
+ params = { id: list1.id }
+
+ issues = described_class.new(project, user, params).execute
+
+ expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
new file mode 100644
index 00000000000..0122159cab8
--- /dev/null
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Boards::Issues::MoveService, services: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+
+ let(:bug) { create(:label, project: project, name: 'Bug') }
+ let(:development) { create(:label, project: project, name: 'Development') }
+ let(:testing) { create(:label, project: project, name: 'Testing') }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:list1) { create(:list, board: board, label: development, position: 0) }
+ let!(:list2) { create(:list, board: board, label: testing, position: 1) }
+ let!(:done) { create(:done_list, board: board) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when moving from backlog' do
+ it 'adds the label of the list it goes to' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: list1.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+
+ context 'when moving to backlog' do
+ it 'removes all list-labels' do
+ issue = create(:labeled_issue, project: project, labels: [bug, development, testing])
+ params = { from_list_id: list1.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug)
+ end
+ end
+
+ context 'when moving from backlog to done' do
+ it 'closes the issue' do
+ issue = create(:labeled_issue, project: project, labels: [bug])
+ params = { from_list_id: backlog.id, to_list_id: done.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving an issue between lists' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list2.id } }
+
+ it 'delegates the label changes to Issues::UpdateService' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes the label from the list it came from and adds the label of the list it goes to' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, testing)
+ end
+ end
+
+ context 'when moving to done' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing]) }
+ let(:params) { { from_list_id: list2.id, to_list_id: done.id } }
+
+ it 'delegates the close proceedings to Issues::CloseService' do
+ expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'removes all list-labels and close the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when moving from done' do
+ let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
+ let(:params) { { from_list_id: done.id, to_list_id: list2.id } }
+
+ it 'delegates the re-open proceedings to Issues::ReopenService' do
+ expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
+
+ described_class.new(project, user, params).execute(issue)
+ end
+
+ it 'adds the label of the list it goes to and reopen the issue' do
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug, testing)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving from done to backlog' do
+ it 'reopens the issue' do
+ issue = create(:labeled_issue, :closed, project: project, labels: [bug])
+ params = { from_list_id: done.id, to_list_id: backlog.id }
+
+ described_class.new(project, user, params).execute(issue)
+ issue.reload
+
+ expect(issue.labels).to contain_exactly(bug)
+ expect(issue).to be_reopened
+ end
+ end
+
+ context 'when moving to same list' do
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { from_list_id: list1.id, to_list_id: list1.id } }
+
+ it 'returns false' do
+ expect(described_class.new(project, user, params).execute(issue)).to eq false
+ end
+
+ it 'keeps issues labels' do
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.reload.labels).to contain_exactly(bug, development)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
new file mode 100644
index 00000000000..5e7e145065e
--- /dev/null
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Boards::Lists::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, name: 'in-progress') }
+
+ subject(:service) { described_class.new(project, user, label_id: label.id) }
+
+ context 'when board lists is empty' do
+ it 'creates a new list at beginning of the list' do
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only a backlog list' do
+ it 'creates a new list at beginning of the list' do
+ create(:backlog_list, board: board)
+
+ list = service.execute
+
+ expect(list.position).to eq 0
+ end
+ end
+
+ context 'when board lists has only labels lists' do
+ it 'creates a new list at end of the lists' do
+ create(:list, board: board, position: 0)
+ create(:list, board: board, position: 1)
+
+ list = service.execute
+
+ expect(list.position).to eq 2
+ end
+ end
+
+ context 'when board lists has backlog, label and done lists' do
+ it 'creates a new list at end of the label lists' do
+ create(:backlog_list, board: board)
+ create(:done_list, board: board)
+ list1 = create(:list, board: board, position: 0)
+
+ list2 = service.execute
+
+ expect(list1.reload.position).to eq 0
+ expect(list2.reload.position).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
new file mode 100644
index 00000000000..6eff445feee
--- /dev/null
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Boards::Lists::DestroyService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ context 'when list type is label' do
+ it 'removes list from board' do
+ list = create(:list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.to change(board.lists, :count).by(-1)
+ end
+
+ it 'decrements position of higher lists' do
+ backlog = create(:backlog_list, board: board)
+ development = create(:list, board: board, position: 0)
+ review = create(:list, board: board, position: 1)
+ staging = create(:list, board: board, position: 2)
+ done = create(:done_list, board: board)
+
+ described_class.new(project, user).execute(development)
+
+ expect(backlog.reload.position).to be_nil
+ expect(review.reload.position).to eq 0
+ expect(staging.reload.position).to eq 1
+ expect(done.reload.position).to be_nil
+ end
+ end
+
+ it 'does not remove list from board when list type is backlog' do
+ list = create(:backlog_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+
+ it 'does not remove list from board when list type is done' do
+ list = create(:done_list, board: board)
+ service = described_class.new(project, user)
+
+ expect { service.execute(list) }.not_to change(board.lists, :count)
+ end
+ end
+end
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
new file mode 100644
index 00000000000..9fd39122737
--- /dev/null
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Boards::Lists::GenerateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ subject(:service) { described_class.new(project, user) }
+
+ context 'when board lists is empty' do
+ it 'creates the default lists' do
+ expect { service.execute }.to change(board.lists, :count).by(4)
+ end
+ end
+
+ context 'when board lists is not empty' do
+ it 'does not creates the default lists' do
+ create(:list, board: board)
+
+ expect { service.execute }.not_to change(board.lists, :count)
+ end
+ end
+
+ context 'when project labels does not contains any list label' do
+ it 'creates labels' do
+ expect { service.execute }.to change(project.labels, :count).by(4)
+ end
+ end
+
+ context 'when project labels contains some of list label' do
+ it 'creates the missing labels' do
+ create(:label, project: project, name: 'Development')
+ create(:label, project: project, name: 'Ready')
+
+ expect { service.execute }.to change(project.labels, :count).by(2)
+ end
+ end
+ end
+end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
new file mode 100644
index 00000000000..3e9b7d07fc6
--- /dev/null
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Boards::Lists::MoveService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+
+ let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:planning) { create(:list, board: board, position: 0) }
+ let!(:development) { create(:list, board: board, position: 1) }
+ let!(:review) { create(:list, board: board, position: 2) }
+ let!(:staging) { create(:list, board: board, position: 3) }
+ let!(:done) { create(:done_list, board: board) }
+
+ context 'when list type is set to label' do
+ it 'keeps position of lists when new position is nil' do
+ service = described_class.new(project, user, position: nil)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to old position' do
+ service = described_class.new(project, user, position: planning.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is negative' do
+ service = described_class.new(project, user, position: -1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is equal to number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when new positon is greater than number of labels lists' do
+ service = described_class.new(project, user, position: board.lists.label.size + 1)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'increments position of intermediate lists when new positon is equal to first position' do
+ service = described_class.new(project, user, position: 0)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [1, 2, 3, 0]
+ end
+
+ it 'decrements position of intermediate lists when new positon is equal to last position' do
+ service = described_class.new(project, user, position: board.lists.label.last.position)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [3, 0, 1, 2]
+ end
+
+ it 'decrements position of intermediate lists when new position is greater than old position' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(planning)
+
+ expect(current_list_positions).to eq [2, 0, 1, 3]
+ end
+
+ it 'increments position of intermediate lists when new position is lower than old position' do
+ service = described_class.new(project, user, position: 1)
+
+ service.execute(staging)
+
+ expect(current_list_positions).to eq [0, 2, 3, 1]
+ end
+ end
+
+ it 'keeps position of lists when list type is backlog' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(backlog)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+
+ it 'keeps position of lists when list type is done' do
+ service = described_class.new(project, user, position: 2)
+
+ service.execute(done)
+
+ expect(current_list_positions).to eq [0, 1, 2, 3]
+ end
+ end
+
+ def current_list_positions
+ [planning, development, review, staging].map { |list| list.reload.position }
+ end
+end
diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb
deleted file mode 100644
index 8b0becd83d3..00000000000
--- a/spec/services/ci/create_builds_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Ci::CreateBuildsService, services: true do
- let(:pipeline) { create(:ci_pipeline, ref: 'master') }
- let(:user) { create(:user) }
-
- describe '#execute' do
- # Using stubbed .gitlab-ci.yml created in commit factory
- #
-
- subject do
- described_class.new(pipeline).execute('test', user, status, nil)
- end
-
- context 'next builds available' do
- let(:status) { 'success' }
-
- it { is_expected.to be_an_instance_of Array }
- it { is_expected.to all(be_an_instance_of Ci::Build) }
-
- it 'does not persist created builds' do
- expect(subject.first).not_to be_persisted
- end
- end
-
- context 'builds skipped' do
- let(:status) { 'skipped' }
-
- it { is_expected.to be_empty }
- end
- end
-end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..4aadd009f3e
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -0,0 +1,214 @@
+require 'spec_helper'
+
+describe Ci::CreatePipelineService, services: true do
+ let(:project) { FactoryGirl.create(:project) }
+ let(:user) { create(:admin) }
+
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ end
+
+ describe '#execute' do
+ def execute(params)
+ described_class.new(project, user, params).execute
+ end
+
+ context 'valid params' do
+ let(:pipeline) do
+ execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+ end
+
+ it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
+ it { expect(pipeline).to be_valid }
+ it { expect(pipeline).to be_persisted }
+ it { expect(pipeline).to eq(project.pipelines.last) }
+ it { expect(pipeline).to have_attributes(user: user) }
+ it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
+ end
+
+ context "skip tag if there is no build for it" do
+ it "creates commit if there is appropriate job" do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+ expect(result).to be_persisted
+ end
+
+ it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+ config = YAML.dump({ deploy: { script: "ls", only: ["master"] } })
+ stub_ci_pipeline_yaml_file(config)
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: "Message" }])
+
+ expect(result).to be_persisted
+ end
+ end
+
+ it 'skips creating pipeline for refs without .gitlab-ci.yml' do
+ stub_ci_pipeline_yaml_file(nil)
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'Message' }])
+
+ expect(result).not_to be_persisted
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+
+ it 'fails commits if yaml is invalid' do
+ message = 'message'
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+ stub_ci_pipeline_yaml_file('invalid: file: file')
+ commits = [{ message: message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq('failed')
+ expect(pipeline.yaml_errors).not_to be_nil
+ end
+
+ context 'when commit contains a [ci skip] directive' do
+ let(:message) { "some message[ci skip]" }
+ let(:messageFlip) { "some message[skip ci]" }
+ let(:capMessage) { "some message[CI SKIP]" }
+ let(:capMessageFlip) { "some message[SKIP CI]" }
+
+ before do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
+ end
+
+ it "skips builds creation if there is [ci skip] tag in commit message" do
+ commits = [{ message: message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [skip ci] tag in commit message" do
+ commits = [{ message: messageFlip }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [CI SKIP] tag in commit message" do
+ commits = [{ message: capMessage }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "skips builds creation if there is [SKIP CI] tag in commit message" do
+ commits = [{ message: capMessageFlip }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("skipped")
+ end
+
+ it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
+ allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
+
+ commits = [{ message: "some message" }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.first.name).to eq("rspec")
+ end
+
+ it "fails builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+ stub_ci_pipeline_yaml_file('invalid: file: fiile')
+ commits = [{ message: message }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.any?).to be false
+ expect(pipeline.status).to eq("failed")
+ expect(pipeline.yaml_errors).not_to be_nil
+ end
+ end
+
+ it "creates commit with failed status if yaml is invalid" do
+ stub_ci_pipeline_yaml_file('invalid: file')
+ commits = [{ message: "some message" }]
+ pipeline = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: commits)
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.status).to eq("failed")
+ expect(pipeline.builds.any?).to be false
+ end
+
+ context 'when there are no jobs for this pipeline' do
+ before do
+ config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).not_to be_persisted
+ expect(Ci::Build.all).to be_empty
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
+
+ context 'with manual actions' do
+ before do
+ config = YAML.dump({ deploy: { script: 'ls', when: 'manual' } })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'does not create a new pipeline' do
+ result = execute(ref: 'refs/heads/master',
+ before: '00000000',
+ after: project.commit.id,
+ commits: [{ message: 'some msg' }])
+
+ expect(result).to be_persisted
+ expect(result.manual_actions).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index ae4b7aca820..d8c443d29d5 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreateTriggerRequestService, services: true do
- let(:service) { Ci::CreateTriggerRequestService.new }
+ let(:service) { described_class.new }
let(:project) { create(:project) }
let(:trigger) { create(:ci_trigger, project: project) }
@@ -9,7 +9,7 @@ describe Ci::CreateTriggerRequestService, services: true do
stub_ci_pipeline_to_return_yaml_file
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
subject { service.execute(project, trigger, 'master') }
@@ -27,8 +27,7 @@ describe Ci::CreateTriggerRequestService, services: true do
subject { service.execute(project, trigger, 'master') }
before do
- stub_ci_pipeline_yaml_file('{}')
- FactoryGirl.create :ci_pipeline, project: project
+ stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
end
it { expect(subject).to be_nil }
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index 476a888e394..c931c3e4829 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,10 +5,10 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:commit) { project.ensure_pipeline(commit_sha, 'master') }
- let(:build) { FactoryGirl.create(:ci_build, pipeline: commit) }
+ let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
- describe :execute do
+ describe '#execute' do
before { build }
context 'branch name' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
new file mode 100644
index 00000000000..8326e5cd313
--- /dev/null
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -0,0 +1,328 @@
+require 'spec_helper'
+
+describe Ci::ProcessPipelineService, services: true do
+ let(:pipeline) { create(:ci_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 create_builds
+ 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)
+ create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
+ end
+
+ it 'processes a pipeline' do
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(2)
+
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(4)
+
+ expect(create_builds).to be_truthy
+ succeed_pending
+ expect(builds.success.count).to eq(5)
+
+ expect(create_builds).to be_falsey
+ end
+
+ it 'does not process pipeline if existing stage is running' do
+ expect(create_builds).to be_truthy
+ expect(builds.pending.count).to eq(2)
+
+ expect(create_builds).to be_falsey
+ expect(builds.pending.count).to eq(2)
+ end
+ end
+
+ context 'custom stage with first job allowed to fail' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+ end
+
+ it 'automatically triggers a next stage when build finishes' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+
+ pipeline.builds.running_or_pending.each(&:drop)
+ expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+ end
+ end
+
+ context 'properly creates builds when "when" is defined' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
+ create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
+ create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
+ create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
+ end
+
+ context 'when builds are successful' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('success')
+ end
+ end
+
+ context 'when test job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when test and test_failure jobs fail' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when deploy job fails' do
+ it 'properly creates builds' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
+ pipeline.builds.running_or_pending.each(&:drop)
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
+ pipeline.reload
+ expect(pipeline.status).to eq('failed')
+ end
+ end
+
+ context 'when build is canceled in the second stage' do
+ it 'does not schedule builds after build has been canceled' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build')
+ expect(builds.pluck(:status)).to contain_exactly('pending')
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.running_or_pending).not_to be_empty
+
+ expect(builds.pluck(:name)).to contain_exactly('build', 'test')
+ expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
+ pipeline.builds.running_or_pending.each(&:cancel)
+
+ expect(builds.running_or_pending).to be_empty
+ expect(pipeline.reload.status).to eq('canceled')
+ end
+ end
+
+ context 'when listing manual actions' do
+ it 'returns only for skipped builds' do
+ # currently all builds are created
+ expect(create_builds).to be_truthy
+ expect(manual_actions).to be_empty
+
+ # succeed stage build
+ pipeline.builds.running_or_pending.each(&:success)
+ expect(manual_actions).to be_empty
+
+ # succeed stage test
+ pipeline.builds.running_or_pending.each(&:success)
+ expect(manual_actions).to be_one # production
+
+ # succeed stage deploy
+ 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
+
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
+ end
+
+ it 'does trigger builds in the next stage' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+ expect(builds.pluck(:name)).to contain_exactly(
+ 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+ end
+ 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'
+ }
+ })
+ end
+
+ # Using stubbed .gitlab-ci.yml created in commit factory
+ #
+
+ 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
+ expect(builds.count).to eq(0)
+ expect(all_builds.count).to eq(2)
+
+ # Create builds will mark the created as pending
+ expect(create_builds).to be_truthy
+ 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)
+ succeed_pending
+ expect(create_builds).to be_truthy
+ expect(builds.success.count).to eq(2)
+ expect(builds.pending.count).to eq(2)
+ expect(all_builds.count).to eq(5)
+
+ # When we succeed the 2 pending from stage test,
+ # We will queue a deploy stage, no new builds will be created
+ succeed_pending
+ expect(create_builds).to be_truthy
+ expect(builds.pending.count).to eq(1)
+ expect(builds.success.count).to eq(4)
+ expect(all_builds.count).to eq(5)
+
+ # When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
+ succeed_pending
+ expect(create_builds).to be_falsey
+ expect(builds.success.count).to eq(5)
+ expect(all_builds.count).to eq(5)
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index f28f2f1438d..026d0ca6534 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -13,7 +13,7 @@ module Ci
specific_runner.assign_to(project)
end
- describe :execute do
+ describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
pending_build.tag_list = ["linux"]
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
deleted file mode 100644
index 309213bd44c..00000000000
--- a/spec/services/create_commit_builds_service_spec.rb
+++ /dev/null
@@ -1,240 +0,0 @@
-require 'spec_helper'
-
-describe CreateCommitBuildsService, services: true do
- let(:service) { CreateCommitBuildsService.new }
- let(:project) { FactoryGirl.create(:empty_project) }
- let(:user) { nil }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- end
-
- describe :execute do
- context 'valid params' do
- let(:pipeline) do
- service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- end
-
- it { expect(pipeline).to be_kind_of(Ci::Pipeline) }
- it { expect(pipeline).to be_valid }
- it { expect(pipeline).to be_persisted }
- it { expect(pipeline).to eq(project.pipelines.last) }
- it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) }
- end
-
- context "skip tag if there is no build for it" do
- it "creates commit if there is appropriate job" do
- result = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- expect(result).to be_persisted
- end
-
- it "creates commit if there is no appropriate job but deploy job has right ref setting" do
- config = YAML.dump({ deploy: { script: "ls", only: ["0_1"] } })
- stub_ci_pipeline_yaml_file(config)
-
- result = service.execute(project, user,
- ref: 'refs/heads/0_1',
- before: '00000000',
- after: '31das312',
- commits: [{ message: "Message" }]
- )
- expect(result).to be_persisted
- end
- end
-
- it 'skips creating pipeline for refs without .gitlab-ci.yml' do
- stub_ci_pipeline_yaml_file(nil)
- result = service.execute(project, user,
- ref: 'refs/heads/0_1',
- before: '00000000',
- after: '31das312',
- commits: [{ message: 'Message' }]
- )
- expect(result).to be_falsey
- expect(Ci::Pipeline.count).to eq(0)
- end
-
- it 'fails commits if yaml is invalid' do
- message = 'message'
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
- stub_ci_pipeline_yaml_file('invalid: file: file')
- commits = [{ message: message }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq('failed')
- expect(pipeline.yaml_errors).not_to be_nil
- end
-
- context 'when commit contains a [ci skip] directive' do
- let(:message) { "some message[ci skip]" }
- let(:messageFlip) { "some message[skip ci]" }
- let(:capMessage) { "some message[CI SKIP]" }
- let(:capMessageFlip) { "some message[SKIP CI]" }
-
- before do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { message }
- end
-
- it "skips builds creation if there is [ci skip] tag in commit message" do
- commits = [{ message: message }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "skips builds creation if there is [skip ci] tag in commit message" do
- commits = [{ message: messageFlip }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "skips builds creation if there is [CI SKIP] tag in commit message" do
- commits = [{ message: capMessage }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "skips builds creation if there is [SKIP CI] tag in commit message" do
- commits = [{ message: capMessageFlip }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- end
-
- it "does not skips builds creation if there is no [ci skip] or [skip ci] tag in commit message" do
- allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { "some message" }
-
- commits = [{ message: "some message" }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.builds.first.name).to eq("staging")
- end
-
- it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
- stub_ci_pipeline_yaml_file('invalid: file: fiile')
- commits = [{ message: message }]
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(pipeline).to be_persisted
- expect(pipeline.builds.any?).to be false
- expect(pipeline.status).to eq("skipped")
- expect(pipeline.yaml_errors).to be_nil
- end
- end
-
- it "skips build creation if there are already builds" do
- allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { gitlab_ci_yaml }
-
- commits = [{ message: "message" }]
- pipeline = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(pipeline).to be_persisted
- expect(pipeline.builds.count(:all)).to eq(2)
-
- pipeline = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(pipeline).to be_persisted
- expect(pipeline.builds.count(:all)).to eq(2)
- end
-
- it "creates commit with failed status if yaml is invalid" do
- stub_ci_pipeline_yaml_file('invalid: file')
-
- commits = [{ message: "some message" }]
-
- pipeline = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(pipeline).to be_persisted
- expect(pipeline.status).to eq("failed")
- expect(pipeline.builds.any?).to be false
- end
-
- context 'when there are no jobs for this pipeline' do
- before do
- config = YAML.dump({ test: { script: 'ls', only: ['feature'] } })
- stub_ci_pipeline_yaml_file(config)
- end
-
- it 'does not create a new pipeline' do
- result = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: [{ message: 'some msg' }])
-
- expect(result).to be_falsey
- expect(Ci::Build.all).to be_empty
- expect(Ci::Pipeline.count).to eq(0)
- end
- end
- end
-end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 654e441f3cd..8da2a2b3c1b 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -89,6 +89,12 @@ describe CreateDeploymentService, services: true do
expect_any_instance_of(described_class).to receive(:execute)
subject
end
+
+ it 'is set as deployable' do
+ subject
+
+ expect(Deployment.last.deployable).to eq(deployable)
+ end
end
context 'without environment specified' do
@@ -105,6 +111,8 @@ describe CreateDeploymentService, services: true do
context 'when build succeeds' do
it_behaves_like 'does create environment and deployment' do
+ let(:deployable) { build }
+
subject { build.success }
end
end
@@ -114,6 +122,14 @@ describe CreateDeploymentService, services: true do
subject { build.drop }
end
end
+
+ context 'when build is retried' do
+ it_behaves_like 'does create environment and deployment' do
+ let(:deployable) { Ci::Build.retry(build) }
+
+ subject { deployable.success }
+ end
+ end
end
end
end
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 7a850066bf8..d81d0fd76c9 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -19,7 +19,7 @@ describe CreateSnippetService, services: true do
@opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
end
- it 'non-admins should not be able to create a public snippet' do
+ it 'non-admins are not able to create a public snippet' do
snippet = create_snippet(nil, @user, @opts)
expect(snippet.errors.messages).to have_key(:visibility_level)
expect(snippet.errors.messages[:visibility_level].first).to(
@@ -27,7 +27,7 @@ describe CreateSnippetService, services: true do
)
end
- it 'admins should be able to create a public snippet' do
+ it 'admins are able to create a public snippet' do
snippet = create_snippet(nil, @admin, @opts)
expect(snippet.errors.any?).to be_falsey
expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb
index a65938fa03b..418a12a83a9 100644
--- a/spec/services/delete_user_service_spec.rb
+++ b/spec/services/delete_user_service_spec.rb
@@ -9,13 +9,15 @@ describe DeleteUserService, services: true do
context 'no options are given' do
it 'deletes the user' do
- DeleteUserService.new(current_user).execute(user)
+ user_data = DeleteUserService.new(current_user).execute(user)
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { user_data['email'].to eq(user.email) }
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'will delete the project in the near future' do
- expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once
+ expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
DeleteUserService.new(current_user).execute(user)
end
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index eca8ddd8ea4..da724643604 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -7,38 +7,52 @@ describe DestroyGroupService, services: true do
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
- context 'database records' do
- before do
- destroy_group(group, user)
+ shared_examples 'group destruction' do |async|
+ context 'database records' do
+ before do
+ destroy_group(group, user, async)
+ end
+
+ it { expect(Group.all).not_to include(group) }
+ it { expect(Project.all).not_to include(project) }
end
- it { expect(Group.all).not_to include(group) }
- it { expect(Project.all).not_to include(project) }
- end
+ context 'file system' do
+ context 'Sidekiq inline' do
+ before do
+ # Run sidekiq immediatly to check that renamed dir will be removed
+ Sidekiq::Testing.inline! { destroy_group(group, user, async) }
+ end
- context 'file system' do
- context 'Sidekiq inline' do
- before do
- # Run sidekiq immediatly to check that renamed dir will be removed
- Sidekiq::Testing.inline! { destroy_group(group, user) }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
end
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey }
- end
+ context 'Sidekiq fake' do
+ before do
+ # Dont run sidekiq to check if renamed repository exists
+ Sidekiq::Testing.fake! { destroy_group(group, user, async) }
+ end
- context 'Sidekiq fake' do
- before do
- # Dont run sidekiq to check if renamed repository exists
- Sidekiq::Testing.fake! { destroy_group(group, user) }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
+ it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
end
+ end
- it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey }
- it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy }
+ def destroy_group(group, user, async)
+ if async
+ DestroyGroupService.new(group, user).async_execute
+ else
+ DestroyGroupService.new(group, user).execute
+ end
end
end
- def destroy_group(group, user)
- DestroyGroupService.new(group, user).execute
+ describe 'asynchronous delete' do
+ it_behaves_like 'group destruction', true
+ end
+
+ describe 'synchronous delete' do
+ it_behaves_like 'group destruction', false
end
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index f6dc9d4008f..16a9956fe7f 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -4,7 +4,7 @@ describe EventCreateService, services: true do
let(:service) { EventCreateService.new }
describe 'Issues' do
- describe :open_issue do
+ describe '#open_issue' do
let(:issue) { create(:issue) }
it { expect(service.open_issue(issue, issue.author)).to be_truthy }
@@ -14,7 +14,7 @@ describe EventCreateService, services: true do
end
end
- describe :close_issue do
+ describe '#close_issue' do
let(:issue) { create(:issue) }
it { expect(service.close_issue(issue, issue.author)).to be_truthy }
@@ -24,7 +24,7 @@ describe EventCreateService, services: true do
end
end
- describe :reopen_issue do
+ describe '#reopen_issue' do
let(:issue) { create(:issue) }
it { expect(service.reopen_issue(issue, issue.author)).to be_truthy }
@@ -36,42 +36,42 @@ describe EventCreateService, services: true do
end
describe 'Merge Requests' do
- describe :open_mr do
+ describe '#open_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.open_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :close_mr do
+ describe '#close_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.close_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :merge_mr do
+ describe '#merge_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.merge_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
- describe :reopen_mr do
+ describe '#reopen_mr' do
let(:merge_request) { create(:merge_request) }
it { expect(service.reopen_mr(merge_request, merge_request.author)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count }
end
end
@@ -80,32 +80,32 @@ describe EventCreateService, services: true do
describe 'Milestone' do
let(:user) { create :user }
- describe :open_milestone do
+ describe '#open_milestone' do
let(:milestone) { create(:milestone) }
it { expect(service.open_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.open_milestone(milestone, user) }.to change { Event.count }
end
end
- describe :close_mr do
+ describe '#close_mr' do
let(:milestone) { create(:milestone) }
it { expect(service.close_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.close_milestone(milestone, user) }.to change { Event.count }
end
end
- describe :destroy_mr do
+ describe '#destroy_mr' do
let(:milestone) { create(:milestone) }
it { expect(service.destroy_milestone(milestone, user)).to be_truthy }
- it "should create new event" do
+ it "creates new event" do
expect { service.destroy_milestone(milestone, user) }.to change { Event.count }
end
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
new file mode 100644
index 00000000000..d019e50649f
--- /dev/null
+++ b/spec/services/files/update_service_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Files::UpdateService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:new_contents) { "New Content" }
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ last_commit_sha: last_commit_sha,
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }.
+ to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to update a file that has changed since you started editing it.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_contents,
+ file_content_encoding: "text",
+ source_project: project,
+ source_branch: project.default_branch,
+ target_branch: project.default_branch,
+ }
+ end
+
+ it "returns a hash with the :success status " do
+ results = subject.execute
+
+ expect(results).to match({ status: :success })
+ end
+
+ it "updates the file with the new contents" do
+ subject.execute
+
+ results = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(results.data).to eq(new_contents)
+ end
+ end
+ end
+end
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 3fc37a315c0..41b0968b8b4 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -17,7 +17,7 @@ describe GitHooksService, services: true do
describe '#execute' do
context 'when receive hooks were successful' do
- it 'should call post-receive hook' do
+ it 'calls post-receive hook' do
hook = double(trigger: [true, nil])
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
@@ -26,7 +26,7 @@ describe GitHooksService, services: true do
end
context 'when pre-receive hook failed' do
- it 'should not call post-receive hook' do
+ it 'does not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
@@ -37,7 +37,7 @@ describe GitHooksService, services: true do
end
context 'when update hook failed' do
- it 'should not call post-receive hook' do
+ it 'does not call post-receive hook' do
expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil])
expect(service).to receive(:run_hook).with('update').and_return([false, ''])
expect(service).not_to receive(:run_hook).with('post-receive')
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index afabeed4a80..6ac1fa8f182 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -7,6 +7,7 @@ describe GitPushService, services: true do
let(:project) { create :project }
before do
+ project.team << [user, :master]
@blankrev = Gitlab::Git::BLANK_SHA
@oldrev = sample_commit.parent_id
@newrev = sample_commit.id
@@ -172,7 +173,7 @@ describe GitPushService, services: true do
describe "Push Event" do
before do
service = execute_service(project, user, @oldrev, @newrev, @ref )
- @event = Event.last
+ @event = Event.find_by_action(Event::PUSHED)
@push_data = service.push_data
end
@@ -224,8 +225,10 @@ describe GitPushService, services: true do
it "when pushing a branch for the first time" do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false })
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
end
it "when pushing a branch for the first time with default branch protection disabled" do
@@ -233,8 +236,8 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).not_to receive(:create)
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).to be_empty
end
it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do
@@ -242,8 +245,23 @@ describe GitPushService, services: true do
expect(project).to receive(:execute_hooks)
expect(project.default_branch).to eq("master")
- expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true })
+
+ execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
+ expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ end
+
+ it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(project).to receive(:execute_hooks)
+ expect(project.default_branch).to eq("master")
execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' )
+ expect(project.protected_branches).not_to be_empty
+ expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER])
end
it "when pushing new commits to existing branch" do
@@ -402,7 +420,7 @@ describe GitPushService, services: true do
context "mentioning an issue" do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
- it "should initiate one api call to jira server to mention the issue" do
+ it "initiates one api call to jira server to mention the issue" do
execute_service(project, user, @oldrev, @newrev, @ref )
expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
@@ -414,7 +432,7 @@ describe GitPushService, services: true do
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
- it "should initiate one api call to jira server to close the issue" do
+ it "initiates one api call to jira server to close the issue" do
transition_body = {
transition: {
id: '2'
@@ -427,7 +445,7 @@ describe GitPushService, services: true do
).once
end
- it "should initiate one api call to jira server to comment on the issue" do
+ it "initiates one api call to jira server to comment on the issue" do
comment_body = {
body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
}.to_json
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
new file mode 100644
index 00000000000..81b1d327696
--- /dev/null
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe ImportExportCleanUpService, services: true do
+ describe '#execute' do
+ let(:service) { described_class.new }
+
+ let(:tmp_import_export_folder) { 'tmp/project_exports' }
+
+ context 'when the import/export directory does not exist' do
+ it 'does not remove any archives' do
+ path = '/invalid/path/'
+ stub_repository_downloads_path(path)
+
+ expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once)
+ expect(service).not_to receive(:clean_up_export_files)
+
+ service.execute
+ end
+ end
+
+ context 'when the import/export directory exists' do
+ it 'removes old files' do
+ in_directory_with_files(mtime: 2.days.ago) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq false }
+ expect(File.directory?(dir)).to eq false
+ end
+ end
+
+ it 'does not remove new files' do
+ in_directory_with_files(mtime: 2.hours.ago) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq true }
+ expect(File.directory?(dir)).to eq true
+ end
+ end
+ end
+
+ def in_directory_with_files(mtime:)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, tmp_import_export_folder, 'subfolder')
+ FileUtils.mkdir_p(dir)
+
+ files = FileUtils.touch(file_list(dir) + [dir], mtime: mtime.to_time)
+
+ yield(dir, files)
+ end
+ end
+
+ def stub_repository_downloads_path(path)
+ new_shared_settings = Settings.shared.merge('path' => path)
+ allow(Settings).to receive(:shared).and_return(new_shared_settings)
+ end
+
+ def file_list(dir)
+ Array.new(5) do |num|
+ File.join(dir, "random-#{num}.tar.gz")
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index 4a689e64dc5..ac08aa53b0b 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -1,118 +1,106 @@
require 'spec_helper'
describe Issues::BulkUpdateService, services: true do
- let(:user) { create(:user) }
- let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, namespace: user.namespace) }
- let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute }
+ def bulk_update(issues, extra_params = {})
+ bulk_update_params = extra_params
+ .reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
- describe :close_issue do
- let(:issues) { create_list(:issue, 5, project: project) }
- let(:params) do
- {
- state_event: 'close',
- issues_ids: issues.map(&:id).join(',')
- }
- end
+ Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
+ end
+
+ describe 'close issues' do
+ let(:issues) { create_list(:issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
+ result = bulk_update(issues, state_event: 'close')
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(issues.count)
end
it 'closes all the issues passed' do
+ bulk_update(issues, state_event: 'close')
+
expect(project.issues.opened).to be_empty
expect(project.issues.closed).not_to be_empty
end
end
- describe :reopen_issues do
- let(:issues) { create_list(:closed_issue, 5, project: project) }
- let(:params) do
- {
- state_event: 'reopen',
- issues_ids: issues.map(&:id).join(',')
- }
- end
+ describe 'reopen issues' do
+ let(:issues) { create_list(:closed_issue, 2, project: project) }
it 'succeeds and returns the correct number of issues updated' do
+ result = bulk_update(issues, state_event: 'reopen')
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(issues.count)
end
it 'reopens all the issues passed' do
+ bulk_update(issues, state_event: 'reopen')
+
expect(project.issues.closed).to be_empty
expect(project.issues.opened).not_to be_empty
end
end
describe 'updating assignee' do
- let(:issue) do
- create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) }
- end
-
- let(:params) do
- {
- assignee_id: assignee_id,
- issues_ids: issue.id.to_s
- }
- end
+ let(:issue) { create(:issue, project: project, assignee: user) }
context 'when the new assignee ID is a valid user' do
- let(:new_assignee) { create(:user) }
- let(:assignee_id) { new_assignee.id }
-
it 'succeeds' do
+ result = bulk_update(issue, assignee_id: create(:user).id)
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'updates the assignee to the use ID passed' do
- expect(issue.reload.assignee).to eq(new_assignee)
+ assignee = create(:user)
+
+ expect { bulk_update(issue, assignee_id: assignee.id) }
+ .to change { issue.reload.assignee }.from(user).to(assignee)
end
end
context 'when the new assignee ID is -1' do
- let(:assignee_id) { -1 }
-
it 'unassigns the issues' do
- expect(issue.reload.assignee).to be_nil
+ expect { bulk_update(issue, assignee_id: -1) }
+ .to change { issue.reload.assignee }.to(nil)
end
end
context 'when the new assignee ID is not present' do
- let(:assignee_id) { nil }
-
it 'does not unassign' do
- expect(issue.reload.assignee).to eq(user)
+ expect { bulk_update(issue, assignee_id: nil) }
+ .not_to change { issue.reload.assignee }
end
end
end
describe 'updating milestones' do
- let(:issue) { create(:issue, project: project) }
+ let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project) }
- let(:params) do
- {
- issues_ids: issue.id.to_s,
- milestone_id: milestone.id
- }
- end
-
it 'succeeds' do
+ result = bulk_update(issue, milestone_id: milestone.id)
+
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
end
it 'updates the issue milestone' do
- expect(project.issues.first.milestone).to eq(milestone)
+ expect { bulk_update(issue, milestone_id: milestone.id) }
+ .to change { issue.reload.milestone }.from(nil).to(milestone)
end
end
describe 'updating labels' do
def create_issue_with_labels(labels)
- create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) }
+ create(:labeled_issue, project: project, labels: labels)
end
let(:bug) { create(:label, project: project) }
@@ -129,15 +117,18 @@ describe Issues::BulkUpdateService, services: true do
let(:add_labels) { [] }
let(:remove_labels) { [] }
- let(:params) do
+ let(:bulk_update_params) do
{
- label_ids: labels.map(&:id),
- add_label_ids: add_labels.map(&:id),
+ label_ids: labels.map(&:id),
+ add_label_ids: add_labels.map(&:id),
remove_label_ids: remove_labels.map(&:id),
- issues_ids: issues.map(&:id).join(',')
}
end
+ before do
+ bulk_update(issues, bulk_update_params)
+ end
+
context 'when label_ids are passed' do
let(:issues) { [issue_all_labels, issue_no_labels] }
let(:labels) { [bug, regression] }
@@ -226,7 +217,7 @@ describe Issues::BulkUpdateService, services: true do
let(:labels) { [merge_requests] }
let(:remove_labels) { [regression] }
- it 'remove the label IDs from all issues passed' do
+ it 'removes the label IDs from all issues passed' do
expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id)
end
@@ -262,4 +253,30 @@ describe Issues::BulkUpdateService, services: true do
end
end
end
+
+ describe 'subscribe to issues' do
+ let(:issues) { create_list(:issue, 2, project: project) }
+
+ it 'subscribes the given user' do
+ bulk_update(issues, subscription_event: 'subscribe')
+
+ expect(issues).to all(be_subscribed(user))
+ 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)
+ end
+ end
+
+ it 'unsubscribes the given user' do
+ bulk_update(issues, subscription_event: 'unsubscribe')
+
+ issues.each do |issue|
+ expect(issue).not_to be_subscribed(user)
+ end
+ end
+ end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 62b25709a5d..aff022a573e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Issues::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
+ let(:guest) { create(:user) }
let(:issue) { create(:issue, assignee: user2) }
let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
@@ -10,26 +11,27 @@ describe Issues::CloseService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context "valid params" do
before do
perform_enqueued_jobs do
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ @issue = described_class.new(project, user, {}).execute(issue)
end
end
it { expect(@issue).to be_valid }
it { expect(@issue).to be_closed }
- it 'should send email to user2 about assign of new issue' do
+ it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(issue.title)
end
- it 'should create system note about issue reassign' do
+ it 'creates system note about issue reassign' do
note = @issue.notes.last
expect(note.note).to include "Status changed to closed"
end
@@ -39,10 +41,22 @@ describe Issues::CloseService, services: true do
end
end
+ context 'current user is not authorized to close issue' do
+ before do
+ perform_enqueued_jobs do
+ @issue = described_class.new(project, guest).execute(issue)
+ end
+ end
+
+ it 'does not close the issue' do
+ expect(@issue).to be_open
+ end
+ end
+
context "external issue tracker" do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ @issue = described_class.new(project, user, {}).execute(issue)
end
it { expect(@issue).to be_valid }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 1ee9f3aae4d..fcc3c0a00bd 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -73,5 +73,7 @@ describe Issues::CreateService, services: true do
end
end
end
+
+ it_behaves_like 'new issuable record that supports slash commands'
end
end
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
new file mode 100644
index 00000000000..34a89fcd4e1
--- /dev/null
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Issues::ReopenService, services: true do
+ let(:guest) { create(:user) }
+ let(:issue) { create(:issue, :closed) }
+ let(:project) { issue.project }
+
+ before do
+ project.team << [guest, :guest]
+ end
+
+ describe '#execute' do
+ context 'current user is not authorized to reopen issue' do
+ before do
+ perform_enqueued_jobs do
+ @issue = described_class.new(project, guest).execute(issue)
+ end
+ end
+
+ it 'does not reopen the issue' do
+ expect(@issue).to be_closed
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index dacbcd8fb46..0313f424463 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -53,7 +53,7 @@ describe Issues::UpdateService, services: true do
it { expect(@issue.labels.count).to eq(1) }
it { expect(@issue.labels.first.title).to eq(label.name) }
- it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+ it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries
email = deliveries.last
recipients = deliveries.last(2).map(&:to).flatten
@@ -61,14 +61,14 @@ describe Issues::UpdateService, services: true do
expect(email.subject).to include(issue.title)
end
- it 'should create system note about issue reassign' do
+ it 'creates system note about issue reassign' do
note = find_note('Reassigned to')
expect(note).not_to be_nil
expect(note.note).to include "Reassigned to \@#{user2.username}"
end
- it 'should create system note about issue label edit' do
+ it 'creates system note about issue label edit' do
note = find_note('Added ~')
expect(note).not_to be_nil
@@ -267,7 +267,7 @@ describe Issues::UpdateService, services: true do
expect(note).to be_nil
end
- it 'should not generate a new note at all' do
+ it 'does not generate a new note at all' do
expect do
update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
end.not_to change { Note.count }
@@ -319,5 +319,10 @@ describe Issues::UpdateService, services: true do
end
end
end
+
+ context 'updating mentions' do
+ let(:mentionable) { issue }
+ include_examples 'updating mentions', Issues::UpdateService
+ end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 782d74ec5ec..232508cda23 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -61,7 +61,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'one commit in the diff' do
- let(:commits) { [commit_1] }
+ let(:commits) { Commit.decorate([commit_1], project) }
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
@@ -84,7 +84,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'commit has no description' do
- let(:commits) { [commit_2] }
+ let(:commits) { Commit.decorate([commit_2], project) }
it 'uses the title of the commit as the title of the merge request' do
expect(merge_request.title).to eq(commit_2.safe_message)
@@ -111,7 +111,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'commit has no description' do
- let(:commits) { [commit_2] }
+ let(:commits) { Commit.decorate([commit_2], project) }
it 'sets the description to "Closes #$issue-iid"' do
expect(merge_request.description).to eq("Closes ##{issue.iid}")
@@ -121,7 +121,7 @@ describe MergeRequests::BuildService, services: true do
end
context 'more than one commit in the diff' do
- let(:commits) { [commit_1, commit_2] }
+ let(:commits) { Commit.decorate([commit_1, commit_2], project) }
it 'allows the merge request to be created' do
expect(merge_request.can_be_created).to eq(true)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 8443a00e70c..24c25e4350f 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe MergeRequests::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
+ let(:guest) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
@@ -10,11 +11,12 @@ describe MergeRequests::CloseService, services: true do
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
- let(:service) { MergeRequests::CloseService.new(project, user, {}) }
+ let(:service) { described_class.new(project, user, {}) }
before do
allow(service).to receive(:execute_hooks)
@@ -32,13 +34,13 @@ describe MergeRequests::CloseService, services: true do
with(@merge_request, 'close')
end
- it 'should send email to user2 about assign of new merge_request' do
+ it 'sends email to user2 about assign of new merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reassign' do
+ it 'creates system note about merge_request reassign' do
note = @merge_request.notes.last
expect(note.note).to include 'Status changed to closed'
end
@@ -47,5 +49,17 @@ describe MergeRequests::CloseService, services: true do
expect(todo.reload).to be_done
end
end
+
+ context 'current user is not authorized to close merge request' do
+ before do
+ perform_enqueued_jobs do
+ @merge_request = described_class.new(project, guest).execute(merge_request)
+ end
+ end
+
+ it 'does not close the merge request' do
+ expect(@merge_request).to be_open
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index e433f49872d..c1e4f8bd96b 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::CreateService, services: true do
let(:user) { create(:user) }
let(:assignee) { create(:user) }
- describe :execute do
+ describe '#execute' do
context 'valid params' do
let(:opts) do
{
@@ -17,7 +17,7 @@ describe MergeRequests::CreateService, services: true do
}
end
- let(:service) { MergeRequests::CreateService.new(project, user, opts) }
+ let(:service) { described_class.new(project, user, opts) }
before do
project.team << [user, :master]
@@ -32,7 +32,7 @@ describe MergeRequests::CreateService, services: true do
it { expect(@merge_request.assignee).to be_nil }
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
- it 'should execute hooks with default action' do
+ it 'executes hooks with default action' do
expect(service).to have_received(:execute_hooks).with(@merge_request)
end
@@ -74,5 +74,14 @@ describe MergeRequests::CreateService, services: true do
end
end
end
+
+ it_behaves_like 'new issuable record that supports slash commands' do
+ let(:default_params) do
+ {
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
new file mode 100644
index 00000000000..8a4b76367e3
--- /dev/null
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -0,0 +1,134 @@
+require "spec_helper"
+
+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_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}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master" }
+
+ describe "#execute" do
+ shared_examples 'new_merge_request_link' do
+ it 'returns url to create new merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: source_branch,
+ url: new_merge_request_url,
+ new_merge_request: true
+ }])
+ end
+ end
+
+ shared_examples 'show_merge_request_url' do
+ it 'returns url to view merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: source_branch,
+ url: show_merge_request_url,
+ new_merge_request: false
+ }])
+ end
+ end
+
+ shared_examples 'no_merge_request_url' do
+ it 'returns no URL' do
+ result = service.execute(changes)
+ expect(result).to be_empty
+ end
+ end
+
+ context 'pushing to default branch' do
+ let(:changes) { default_branch_changes }
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing to project with MRs disabled' do
+ let(:changes) { new_branch_changes }
+
+ before do
+ project.merge_requests_enabled = false
+ end
+
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing one completely new branch' do
+ let(:changes) { new_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to existing branch but no merge request' do
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to deleted branch' do
+ let(:changes) { deleted_branch_changes }
+ it_behaves_like 'no_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request opened' do
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request is reopened' do
+ let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch from forked project' do
+ let(:user) { create(:user) }
+ let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ # Source project is now the forked one
+ let(:service) { MergeRequests::GetUrlsService.new(forked_project) }
+
+ before do
+ allow(forked_project).to receive(:empty_repo?).and_return(false)
+ end
+
+ it_behaves_like 'show_merge_request_url'
+ end
+
+ context 'pushing to existing branch and merge request is closed' do
+ let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing to existing branch and merge request is merged' do
+ let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) }
+ let(:changes) { existing_branch_changes }
+ it_behaves_like 'new_merge_request_link'
+ end
+
+ context 'pushing new branch and existing branch (with merge request created) at once' do
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") }
+ 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" }
+
+ it 'returns 2 urls for both creating new and showing merge request' do
+ result = service.execute(changes)
+ expect(result).to match([{
+ branch_name: "new_branch",
+ url: new_merge_request_url,
+ new_merge_request: true
+ }, {
+ branch_name: "existing_branch",
+ url: show_merge_request_url,
+ new_merge_request: false
+ }])
+ end
+ end
+ end
+end
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
new file mode 100644
index 00000000000..807f89e80b7
--- /dev/null
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe MergeRequests::MergeRequestDiffCacheService do
+ let(:subject) { MergeRequests::MergeRequestDiffCacheService.new }
+
+ describe '#execute' do
+ it 'retrieves the diff files to cache the highlighted result' do
+ merge_request = create(:merge_request)
+ cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options]
+
+ expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
+ expect(Rails.cache).to receive(:write).with(cache_key, anything)
+
+ subject.execute(merge_request)
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 2f72cd60071..159f6817e8d 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -11,7 +11,7 @@ describe MergeRequests::MergeService, services: true do
project.team << [user2, :developer]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
@@ -26,13 +26,13 @@ describe MergeRequests::MergeService, services: true do
it { expect(merge_request).to be_valid }
it { expect(merge_request).to be_merged }
- it 'should send email to user2 about merge of new merge_request' do
+ it 'sends email to user2 about merge of new merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request merge' do
+ it 'creates system note about merge_request merge' do
note = merge_request.notes.last
expect(note.note).to include 'Status changed to merged'
end
@@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do
expect(merge_request.merge_error).to eq("error")
end
+
+ it 'aborts if there is a merge conflict' do
+ allow_any_instance_of(Repository).to receive(:merge).and_return(false)
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request)
+
+ expect(merge_request.open?).to be_truthy
+ expect(merge_request.merge_commit_sha).to be_nil
+ expect(merge_request.merge_error).to eq("Conflicts detected during merge")
+ end
end
end
end
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
index 4da8146e3d6..520e906b21f 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -110,19 +110,15 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
context 'properly handles multiple stages' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
- let(:build) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
- let(:test) { create(:ci_build, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+ let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
+ let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
+ let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
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
-
- # We create test after the build
- allow(pipeline).to receive(:create_next_builds).and_wrap_original do
- test
- end
end
it "doesn't merge if some stages failed" do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7d5cb876063..fff86480c6d 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -5,7 +5,7 @@ describe MergeRequests::RefreshService, services: true do
let(:user) { create(:user) }
let(:service) { MergeRequests::RefreshService }
- describe :execute do
+ describe '#execute' do
before do
@user = create(:user)
group = create(:group)
@@ -55,9 +55,9 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it 'should execute hooks with update action' do
+ it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
- with(@merge_request, 'update')
+ with(@merge_request, 'update', @oldrev)
end
it { expect(@merge_request.notes).not_to be_empty }
@@ -88,8 +88,7 @@ describe MergeRequests::RefreshService, services: true do
# Merge master -> feature branch
author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
commit_options = { message: 'Test message', committer: author, author: author }
- master_commit = @project.repository.commit('master')
- @project.repository.merge(@user, master_commit.id, 'feature', commit_options)
+ @project.repository.merge(@user, @merge_request, commit_options)
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
@@ -112,9 +111,9 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it 'should execute hooks with update action' do
+ it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
- with(@fork_merge_request, 'update')
+ with(@fork_merge_request, 'update', @oldrev)
end
it { expect(@merge_request.notes).to be_empty }
@@ -159,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do
it 'refreshes the merge request' do
expect(refresh_service).to receive(:execute_hooks).
- with(@fork_merge_request, 'update')
+ with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA)
allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev)
refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master')
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index ac0221998f5..af7424a76a9 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -3,22 +3,23 @@ require 'spec_helper'
describe MergeRequests::ReopenService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2) }
+ let(:guest) { create(:user) }
+ let(:merge_request) { create(:merge_request, :closed, assignee: user2) }
let(:project) { merge_request.project }
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [guest, :guest]
end
- describe :execute do
+ describe '#execute' do
context 'valid params' do
- let(:service) { MergeRequests::ReopenService.new(project, user, {}) }
+ let(:service) { described_class.new(project, user, {}) }
before do
allow(service).to receive(:execute_hooks)
- merge_request.state = :closed
perform_enqueued_jobs do
service.execute(merge_request)
end
@@ -27,21 +28,33 @@ describe MergeRequests::ReopenService, services: true do
it { expect(merge_request).to be_valid }
it { expect(merge_request).to be_reopened }
- it 'should execute hooks with reopen action' do
+ it 'executes hooks with reopen action' do
expect(service).to have_received(:execute_hooks).
with(merge_request, 'reopen')
end
- it 'should send email to user2 about reopen of merge_request' do
+ it 'sends email to user2 about reopen of merge_request' do
email = ActionMailer::Base.deliveries.last
expect(email.to.first).to eq(user2.email)
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reopen' do
+ it 'creates system note about merge_request reopen' do
note = merge_request.notes.last
expect(note.note).to include 'Status changed to reopened'
end
end
+
+ context 'current user is not authorized to reopen merge request' do
+ before do
+ perform_enqueued_jobs do
+ @merge_request = described_class.new(project, guest).execute(merge_request)
+ end
+ end
+
+ it 'does not reopen the merge request' do
+ expect(@merge_request).to be_closed
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service.rb
new file mode 100644
index 00000000000..7ddd812e513
--- /dev/null
+++ b/spec/services/merge_requests/resolved_discussion_notification_service.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolvedDiscussionNotificationService, services: true do
+ let(:merge_request) { create(:merge_request) }
+ let(:user) { create(:user) }
+ let(:project) { merge_request.project }
+ subject { described_class.new(project, user) }
+
+ describe "#execute" do
+ context "when not all discussions are resolved" do
+ before do
+ allow(merge_request).to receive(:discussions_resolved?).and_return(false)
+ end
+
+ it "doesn't add a system note" do
+ expect(SystemNoteService).not_to receive(:resolve_all_discussions)
+
+ subject.execute(merge_request)
+ end
+
+ it "doesn't send a notification email" do
+ expect_any_instance_of(NotificationService).not_to receive(:resolve_all_discussions)
+
+ subject.execute(merge_request)
+ end
+ end
+
+ context "when all discussions are resolved" do
+ before do
+ allow(merge_request).to receive(:discussions_resolved?).and_return(true)
+ end
+
+ it "adds a system note" do
+ expect(SystemNoteService).to receive(:resolve_all_discussions).with(merge_request, project, user)
+
+ subject.execute(merge_request)
+ end
+
+ it "sends a notification email" do
+ expect_any_instance_of(NotificationService).to receive(:resolve_all_discussions).with(merge_request, user)
+
+ subject.execute(merge_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index d4ebe28c276..6dfeb581975 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -64,12 +64,12 @@ describe MergeRequests::UpdateService, services: true do
it { expect(@merge_request.target_branch).to eq('target') }
it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') }
- it 'should execute hooks with update action' do
+ it 'executes hooks with update action' do
expect(service).to have_received(:execute_hooks).
with(@merge_request, 'update')
end
- it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+ it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
deliveries = ActionMailer::Base.deliveries
email = deliveries.last
recipients = deliveries.last(2).map(&:to).flatten
@@ -77,14 +77,14 @@ describe MergeRequests::UpdateService, services: true do
expect(email.subject).to include(merge_request.title)
end
- it 'should create system note about merge_request reassign' do
+ it 'creates system note about merge_request reassign' do
note = find_note('Reassigned to')
expect(note).not_to be_nil
expect(note.note).to include "Reassigned to \@#{user2.username}"
end
- it 'should create system note about merge_request label edit' do
+ it 'creates system note about merge_request label edit' do
note = find_note('Added ~')
expect(note).not_to be_nil
@@ -226,6 +226,11 @@ describe MergeRequests::UpdateService, services: true do
end
end
+ context 'updating mentions' do
+ let(:mentionable) { merge_request }
+ include_examples 'updating mentions', MergeRequests::UpdateService
+ end
+
context 'when MergeRequest has tasks' do
before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 1cd6eb2ab38..5d400299be0 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -9,7 +9,7 @@ describe Milestones::CloseService, services: true do
project.team << [user, :master]
end
- describe :execute do
+ describe '#execute' do
before do
Milestones::CloseService.new(project, user, {}).execute(milestone)
end
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index c793026e300..6d29edb449a 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -4,7 +4,7 @@ describe Milestones::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- describe :execute do
+ describe '#execute' do
context "valid params" do
before do
project.team << [user, :master]
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 35f576874b8..93885c84dc3 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -4,22 +4,36 @@ describe Notes::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
+ let(:opts) do
+ { note: 'Awesome comment', noteable_type: 'Issue', noteable_id: issue.id }
+ end
+
+ describe '#execute' do
+ before do
+ project.team << [user, :master]
+ end
- describe :execute do
context "valid params" do
before do
- project.team << [user, :master]
- opts = {
- note: 'Awesome comment',
- noteable_type: 'Issue',
- noteable_id: issue.id
- }
-
@note = Notes::CreateService.new(project, user, opts).execute
end
it { expect(@note).to be_valid }
- it { expect(@note.note).to eq('Awesome comment') }
+ it { expect(@note.note).to eq(opts[:note]) }
+ end
+
+ describe 'note with commands' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) }
+
+ it 'saves the note and does not alter the note text' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original
+
+ note = described_class.new(project, user, opts.merge(note: note_text)).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ end
end
end
@@ -42,7 +56,7 @@ describe Notes::CreateService, services: true do
it "creates regular note if emoji name is invalid" do
opts = {
- note: ':smile: moretext: ',
+ note: ':smile: moretext:',
noteable_type: 'Issue',
noteable_id: issue.id
}
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index d4c50f824c1..e33a611929b 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -5,7 +5,7 @@ describe Notes::PostProcessService, services: true do
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
- describe :execute do
+ describe '#execute' do
before do
project.team << [user, :master]
note_opts = {
diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/slash_commands_service_spec.rb
new file mode 100644
index 00000000000..4f231aab161
--- /dev/null
+++ b/spec/services/notes/slash_commands_service_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Notes::SlashCommandsService, services: true do
+ shared_context 'note on noteable' do
+ let(:project) { create(:empty_project) }
+ let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:assignee) { create(:user) }
+ end
+
+ shared_examples 'note on noteable that does not support slash commands' do
+ include_context 'note on noteable'
+
+ before do
+ note.note = note_text
+ end
+
+ describe 'note with only command' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(/close\n/assign @#{assignee.username}") }
+
+ it 'saves the note and does not alter the note text' do
+ content, command_params = service.extract_commands(note)
+
+ expect(content).to eq note_text
+ expect(command_params).to be_empty
+ end
+ end
+ end
+
+ describe 'note with command & text' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(HELLO\n/close\n/assign @#{assignee.username}\nWORLD) }
+
+ it 'saves the note and does not alter the note text' do
+ content, command_params = service.extract_commands(note)
+
+ expect(content).to eq note_text
+ expect(command_params).to be_empty
+ end
+ end
+ end
+ end
+
+ shared_examples 'note on noteable that supports slash commands' do
+ include_context 'note on noteable'
+
+ before do
+ note.note = note_text
+ end
+
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:labels) { create_pair(:label, project: project) }
+
+ describe 'note with only command' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) do
+ %(/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ end
+
+ it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignee).to eq(assignee)
+ expect(note.noteable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close!
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { '/reopen' }
+
+ it 'opens the noteable, and leave no note' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq ''
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+
+ describe 'note with command & text' do
+ describe '/close, /label, /assign & /milestone' do
+ let(:note_text) do
+ %(HELLO\n/close\n/label ~#{labels.first.name} ~#{labels.last.name}\n/assign @#{assignee.username}\n/milestone %"#{milestone.name}"\nWORLD)
+ end
+
+ it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_closed
+ expect(note.noteable.labels).to match_array(labels)
+ expect(note.noteable.assignee).to eq(assignee)
+ expect(note.noteable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/reopen' do
+ before do
+ note.noteable.close
+ expect(note.noteable).to be_closed
+ end
+ let(:note_text) { "HELLO\n/reopen\nWORLD" }
+
+ it 'opens the noteable' do
+ content, command_params = service.extract_commands(note)
+ service.execute(command_params, note)
+
+ expect(content).to eq "HELLO\nWORLD"
+ expect(note.noteable).to be_open
+ end
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, master) }
+
+ it_behaves_like 'note on noteable that supports slash commands' do
+ let(:note) { build(:note_on_issue, project: project) }
+ end
+
+ it_behaves_like 'note on noteable that supports slash commands' do
+ let(:note) { build(:note_on_merge_request, project: project) }
+ end
+
+ it_behaves_like 'note on noteable that does not support slash commands' do
+ let(:note) { build(:note_on_commit, project: project) }
+ end
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 54719cbb8d8..f81a58899fd 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -9,13 +9,35 @@ describe NotificationService, services: true do
end
end
+ shared_examples 'notifications for new mentions' do
+ def send_notifications(*new_mentions)
+ reset_delivered_emails!
+ notification.send(notification_method, mentionable, new_mentions, @u_disabled)
+ end
+
+ it 'sends no emails when no new mentions are present' do
+ send_notifications
+ expect(ActionMailer::Base.deliveries).to be_empty
+ end
+
+ it 'emails new mentions with a watch level higher than participant' do
+ send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ should_only_email(@u_watcher, @u_participant_mentioned, @u_custom_global)
+ end
+
+ 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
+ end
+ end
+
describe 'Keys' do
describe '#new_key' do
let!(:key) { create(:personal_key) }
it { expect(notification.new_key(key)).to be_truthy }
- it 'should sent email to key owner' do
+ it 'sends email to key owner' do
expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
@@ -27,7 +49,7 @@ describe NotificationService, services: true do
it { expect(notification.new_email(email)).to be_truthy }
- it 'should send email to email owner' do
+ it 'sends email to email owner' do
expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
@@ -50,7 +72,7 @@ describe NotificationService, services: true do
update_custom_notification(:new_note, @u_custom_global)
end
- describe :new_note do
+ describe '#new_note' do
it do
add_users_with_subscription(note.project, issue)
@@ -293,6 +315,30 @@ describe NotificationService, services: true do
end
end
end
+
+ context "merge request diff note" do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+
+ before do
+ build_team(note.project)
+ project.team << [merge_request.author, :master]
+ project.team << [merge_request.assignee, :master]
+ end
+
+ describe '#new_note' do
+ it "records sent notifications" do
+ # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
+ expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(4).times.and_call_original
+
+ notification.new_note(note)
+
+ expect(SentNotification.last.position).to eq(note.position)
+ end
+ end
+ end
end
describe 'Issues' do
@@ -375,6 +421,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_issue' do
+ let(:notification_method) { :new_mentions_in_issue }
+ let(:mentionable) { issue }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_issue' do
before do
update_custom_notification(:reassign_issue, @u_guest_custom, project)
@@ -569,7 +622,7 @@ describe NotificationService, services: true do
update_custom_notification(:close_issue, @u_custom_global)
end
- it 'should sent email to issue assignee and issue author' do
+ it 'sends email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -622,7 +675,7 @@ describe NotificationService, services: true do
update_custom_notification(:reopen_issue, @u_custom_global)
end
- it 'should send email to issue assignee and issue author' do
+ it 'sends email to issue assignee and issue author' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -676,6 +729,8 @@ describe NotificationService, services: true do
before do
build_team(merge_request.target_project)
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
end
@@ -739,6 +794,13 @@ describe NotificationService, services: true do
end
end
+ describe '#new_mentions_in_merge_request' do
+ let(:notification_method) { :new_mentions_in_merge_request }
+ let(:mentionable) { merge_request }
+
+ include_examples 'notifications for new mentions'
+ end
+
describe '#reassigned_merge_request' do
before do
update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
@@ -980,6 +1042,52 @@ describe NotificationService, services: true do
end
end
end
+
+ describe "#resolve_all_discussions" do
+ it do
+ notification.resolve_all_discussions(merge_request, @u_disabled)
+
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_email(@u_guest_watcher)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ context 'participating' do
+ context 'by assignee' do
+ before do
+ merge_request.update_attribute(:assignee, @u_lazy_participant)
+ notification.resolve_all_discussions(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by note' do
+ let!(:note) { create(:note_on_issue, noteable: merge_request, project_id: project.id, note: 'anything', author: @u_lazy_participant) }
+
+ before { notification.resolve_all_discussions(merge_request, @u_disabled) }
+
+ it { should_email(@u_lazy_participant) }
+ end
+
+ context 'by author' do
+ before do
+ merge_request.author = @u_lazy_participant
+ merge_request.save
+ notification.resolve_all_discussions(merge_request, @u_disabled)
+ end
+
+ it { should_email(@u_lazy_participant) }
+ end
+ end
+ end
end
describe 'Projects' do
@@ -1005,6 +1113,46 @@ describe NotificationService, services: true do
end
end
+ describe 'GroupMember' do
+ describe '#decline_group_invite' do
+ let(:creator) { create(:user) }
+ let(:group) { create(:group) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ group.add_owner(creator)
+ group.add_developer(member, creator)
+ end
+
+ it do
+ group_member = group.members.first
+
+ expect do
+ notification.decline_group_invite(group_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
+ describe 'ProjectMember' do
+ describe '#decline_group_invite' do
+ let(:project) { create(:project) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ project.team << [member, :developer, project.owner]
+ end
+
+ it do
+ project_member = project.members.first
+
+ expect do
+ notification.decline_project_invite(project_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 0971fec2e9f..7916c2d957c 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -13,7 +13,7 @@ describe Projects::AutocompleteService, services: true do
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) }
- it 'should not list project confidential issues for guests' do
+ it 'does not list project confidential issues for guests' do
autocomplete = described_class.new(project, nil)
issues = autocomplete.issues.map(&:iid)
@@ -23,7 +23,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
- it 'should not list project confidential issues for non project members' do
+ it 'does not list project confidential issues for non project members' do
autocomplete = described_class.new(project, non_member)
issues = autocomplete.issues.map(&:iid)
@@ -33,7 +33,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
- it 'should not list project confidential issues for project members with guest role' do
+ it 'does not list project confidential issues for project members with guest role' do
project.team << [member, :guest]
autocomplete = described_class.new(project, non_member)
@@ -45,7 +45,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 1
end
- it 'should list project confidential issues for author' do
+ it 'lists project confidential issues for author' do
autocomplete = described_class.new(project, author)
issues = autocomplete.issues.map(&:iid)
@@ -55,7 +55,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 2
end
- it 'should list project confidential issues for assignee' do
+ it 'lists project confidential issues for assignee' do
autocomplete = described_class.new(project, assignee)
issues = autocomplete.issues.map(&:iid)
@@ -65,7 +65,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 2
end
- it 'should list project confidential issues for project members' do
+ it 'lists project confidential issues for project members' do
project.team << [member, :developer]
autocomplete = described_class.new(project, member)
@@ -77,7 +77,7 @@ describe Projects::AutocompleteService, services: true do
expect(issues.count).to eq 3
end
- it 'should list all project issues for admin' do
+ it 'lists all project issues for admin' do
autocomplete = described_class.new(project, admin)
issues = autocomplete.issues.map(&:iid)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index fd114359467..bbced59ff02 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -109,7 +109,7 @@ describe Projects::CreateService, services: true do
)
end
- it 'should not allow a restricted visibility level for non-admins' do
+ 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)
@@ -118,7 +118,7 @@ describe Projects::CreateService, services: true do
)
end
- it 'should allow a restricted visibility level for admins' do
+ it 'allows a restricted visibility level for admins' do
admin = create(:admin)
project = create_project(admin, @opts)
@@ -128,7 +128,7 @@ describe Projects::CreateService, services: true do
end
context 'repository creation' do
- it 'should synchronously create the repository' do
+ it 'synchronously creates the repository' do
expect_any_instance_of(Project).to receive(:create_repository)
project = create_project(@user, @opts)
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index f252e2c5902..122a7cea2a1 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -35,8 +35,6 @@ describe Projects::DownloadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to be true }
it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
it { expect(@link_to_file[:alt]).to eq('rails_sample') }
end
@@ -49,8 +47,6 @@ describe Projects::DownloadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to be false }
it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') }
end
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
new file mode 100644
index 00000000000..a37510cf159
--- /dev/null
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Projects::EnableDeployKeyService, services: true do
+ let(:deploy_key) { create(:deploy_key, public: true) }
+ let(:project) { create(:empty_project) }
+ let(:user) { project.creator}
+ let!(:params) { { key_id: deploy_key.id } }
+
+ it 'enables the key' do
+ expect do
+ service.execute
+ end.to change { project.deploy_keys.count }.from(0).to(1)
+ end
+
+ context 'trying to add an unaccessable key' do
+ let(:another_key) { create(:another_key) }
+ let!(:params) { { key_id: another_key.id } }
+
+ it 'returns nil if the key cannot be added' do
+ expect(service.execute).to be nil
+ end
+ end
+
+ def service
+ Projects::EnableDeployKeyService.new(project, user, params)
+ end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 31bb7120d84..ef2036c78b1 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -26,7 +26,7 @@ describe Projects::ForkService, services: true do
end
context 'project already exists' do
- it "should fail due to validation, not transaction failure" do
+ it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy
@@ -36,7 +36,7 @@ describe Projects::ForkService, services: true do
end
context 'GitLab CI is enabled' do
- it "fork and enable CI for fork" do
+ it "forks and enables CI for fork" do
@from_project.enable_ci
@to_project = fork_project(@from_project, @to_user)
expect(@to_project.builds_enabled?).to be_truthy
@@ -97,14 +97,14 @@ describe Projects::ForkService, services: true do
end
context 'fork project for group when user not owner' do
- it 'group developer should fail to fork project into the group' do
+ it 'group developer fails to fork project into the group' do
to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
context 'project already exists in group' do
- it 'should fail due to validation, not transaction failure' do
+ it 'fails due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name,
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts)
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index bd4dc6a0f79..ad0d58672b3 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -12,18 +12,28 @@ describe Projects::HousekeepingService do
it 'enqueues a sidekiq job' do
expect(subject).to receive(:try_obtain_lease).and_return(true)
- expect(GitlabShellOneShotWorker).to receive(:perform_async).with(:gc, project.repository_storage_path, project.path_with_namespace)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id)
subject.execute
- expect(project.pushes_since_gc).to eq(0)
+ expect(project.reload.pushes_since_gc).to eq(0)
end
- it 'does not enqueue a job when no lease can be obtained' do
- expect(subject).to receive(:try_obtain_lease).and_return(false)
- expect(GitlabShellOneShotWorker).not_to receive(:perform_async)
+ context 'when no lease can be obtained' do
+ before(:each) do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ end
- expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
- expect(project.pushes_since_gc).to eq(0)
+ it 'does not enqueue a job' do
+ expect(GitGarbageCollectWorker).not_to receive(:perform_async)
+
+ expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ end
+
+ it 'does not reset pushes_since_gc' do
+ expect do
+ expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken)
+ end.not_to change { project.pushes_since_gc }.from(3)
+ end
end
end
@@ -39,10 +49,24 @@ describe Projects::HousekeepingService do
end
describe 'increment!' do
+ let(:lease_key) { "project_housekeeping:increment!:#{project.id}" }
+
it 'increments the pushes_since_gc counter' do
- expect(project.pushes_since_gc).to eq(0)
- subject.increment!
- expect(project.pushes_since_gc).to eq(1)
+ lease = double(:lease, try_obtain: true)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
+
+ expect do
+ subject.increment!
+ end.to change { project.pushes_since_gc }.from(0).to(1)
+ end
+
+ it 'does not increment when no lease can be obtained' do
+ lease = double(:lease, try_obtain: false)
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(lease_key, anything).and_return(lease)
+
+ expect do
+ subject.increment!
+ end.not_to change { project.pushes_since_gc }
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index e8b9e6b9238..e139be19140 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -9,7 +9,7 @@ describe Projects::UpdateService, services: true do
@opts = {}
end
- context 'should be private when updated to private' do
+ context 'is private when updated to private' do
before do
@created_private = @project.private?
@@ -21,7 +21,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be internal when updated to internal' do
+ context 'is internal when updated to internal' do
before do
@created_private = @project.private?
@@ -33,7 +33,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.internal?).to be_truthy }
end
- context 'should be public when updated to public' do
+ context 'is public when updated to public' do
before do
@created_private = @project.private?
@@ -50,7 +50,7 @@ describe Projects::UpdateService, services: true do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
end
- context 'should be private when updated to private' do
+ context 'is private when updated to private' do
before do
@created_private = @project.private?
@@ -62,7 +62,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be internal when updated to internal' do
+ context 'is internal when updated to internal' do
before do
@created_private = @project.private?
@@ -74,7 +74,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.internal?).to be_truthy }
end
- context 'should be private when updated to public' do
+ context 'is private when updated to public' do
before do
@created_private = @project.private?
@@ -86,7 +86,7 @@ describe Projects::UpdateService, services: true do
it { expect(@project.private?).to be_truthy }
end
- context 'should be public when updated to public by admin' do
+ context 'is public when updated to public by admin' do
before do
@created_private = @project.private?
@@ -114,7 +114,7 @@ describe Projects::UpdateService, services: true do
@fork_created_internal = forked_project.internal?
end
- context 'should update forks visibility level when parent set to more restrictive' do
+ context 'updates forks visibility level when parent set to more restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
update_project(project, user, opts).inspect
@@ -126,7 +126,7 @@ describe Projects::UpdateService, services: true do
it { expect(project.forks.first.private?).to be_truthy }
end
- context 'should not update forks visibility level when parent set to less restrictive' do
+ context 'does not update forks visibility level when parent set to less restrictive' do
before do
opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
update_project(project, user, opts).inspect
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index 9268a9fb1a2..c42eeba4b9c 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -15,9 +15,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('banana_sample') }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('banana_sample.gif') }
end
@@ -31,8 +29,6 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
it { expect(@link_to_file).to have_value('dk') }
- it { expect(@link_to_file).to have_key(:is_image) }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('dk.png') }
end
@@ -44,9 +40,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('rails_sample') }
- it { expect(@link_to_file[:is_image]).to equal(true) }
it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
end
@@ -58,9 +52,7 @@ describe Projects::UploadService, services: true do
it { expect(@link_to_file).to have_key(:alt) }
it { expect(@link_to_file).to have_key(:url) }
- it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
- it { expect(@link_to_file[:is_image]).to equal(false) }
it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
end
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
index ce7d1455975..87192457298 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -6,14 +6,14 @@ describe RepairLdapBlockedUserService, services: true do
subject(:service) { RepairLdapBlockedUserService.new(user) }
describe '#execute' do
- it 'change to normal block after destroying last ldap identity' do
+ it 'changes to normal block after destroying last ldap identity' do
identity.destroy
service.execute
expect(user.reload).not_to be_ldap_blocked
end
- it 'change to normal block after changing last ldap identity to another provider' do
+ it 'changes to normal block after changing last ldap identity to another provider' do
identity.update_attribute(:provider, 'twitter')
service.execute
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
new file mode 100644
index 00000000000..842585f9e54
--- /dev/null
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe RepositoryArchiveCleanUpService, services: true do
+ describe '#execute' do
+ subject(:service) { described_class.new }
+
+ context 'when the downloads directory does not exist' do
+ it 'does not remove any archives' do
+ path = '/invalid/path/'
+ stub_repository_downloads_path(path)
+
+ expect(File).to receive(:directory?).with(path).and_return(false)
+ expect(service).not_to receive(:clean_up_old_archives)
+ expect(service).not_to receive(:clean_up_empty_directories)
+
+ service.execute
+ end
+ end
+
+ context 'when the downloads directory exists' do
+ shared_examples 'invalid archive files' do |dirname, extensions, mtime|
+ it 'does not remove files and directoy' do
+ in_directory_with_files(dirname, extensions, mtime) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq true }
+ expect(File.directory?(dir)).to eq true
+ end
+ end
+ end
+
+ it 'removes files older than 2 hours that matches valid archive extensions' do
+ in_directory_with_files('sample.git', %w[tar tar.bz2 tar.gz zip], 2.hours) do |dir, files|
+ service.execute
+
+ files.each { |file| expect(File.exist?(file)).to eq false }
+ expect(File.directory?(dir)).to eq false
+ end
+ end
+
+ context 'with files older than 2 hours that does not matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 2.hours
+ end
+
+ context 'with files older than 2 hours inside invalid directories' do
+ it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours
+ end
+
+ context 'with files newer than 2 hours that matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[tar tar.bz2 tar.gz zip], 1.hour
+ end
+
+ context 'with files newer than 2 hours that does not matches valid archive extensions' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 1.hour
+ end
+
+ context 'with files newer than 2 hours inside invalid directories' do
+ it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour
+ end
+ end
+
+ def in_directory_with_files(dirname, extensions, mtime)
+ Dir.mktmpdir do |tmpdir|
+ stub_repository_downloads_path(tmpdir)
+ dir = File.join(tmpdir, dirname)
+ files = create_temporary_files(dir, extensions, mtime)
+
+ yield(dir, files)
+ end
+ end
+
+ def stub_repository_downloads_path(path)
+ allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path)
+ end
+
+ def create_temporary_files(dir, extensions, mtime)
+ FileUtils.mkdir_p(dir)
+ FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime)
+ end
+ end
+end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index 7b3a9a75d7c..bd89c4a7c11 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -16,7 +16,7 @@ describe 'Search::GlobalService', services: true do
describe '#execute' do
context 'unauthenticated' do
- it 'should return public projects only' do
+ it 'returns public projects only' do
context = Search::GlobalService.new(nil, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [public_project]
@@ -24,19 +24,19 @@ describe 'Search::GlobalService', services: true do
end
context 'authenticated' do
- it 'should return public, internal and private projects' do
+ it 'returns public, internal and private projects' do
context = Search::GlobalService.new(user, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
end
- it 'should return only public & internal projects' do
+ it 'returns only public & internal projects' do
context = Search::GlobalService.new(internal_user, search: "searchable")
results = context.execute
expect(results.objects('projects')).to match_array [internal_project, public_project]
end
- it 'namespace name should be searchable' do
+ it 'namespace name is searchable' do
context = Search::GlobalService.new(user, search: found_project.namespace.path)
results = context.execute
expect(results.objects('projects')).to match_array [found_project]
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
new file mode 100644
index 00000000000..a616275e883
--- /dev/null
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -0,0 +1,384 @@
+require 'spec_helper'
+
+describe SlashCommands::InterpretService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project, title: '9.10') }
+ let(:inprogress) { create(:label, project: project, title: 'In Progress') }
+ let(:bug) { create(:label, project: project, title: 'Bug') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe '#execute' do
+ let(:service) { described_class.new(project, user) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ shared_examples 'reopen command' do
+ it 'returns state_event: "reopen" if content contains /reopen' do
+ issuable.close!
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(state_event: 'reopen')
+ end
+ end
+
+ shared_examples 'close command' do
+ it 'returns state_event: "close" if content contains /close' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(state_event: 'close')
+ end
+ end
+
+ shared_examples 'title command' do
+ it 'populates title: "A brand new title" if content contains /title A brand new title' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(title: 'A brand new title')
+ end
+ end
+
+ shared_examples 'assign command' do
+ it 'fetches assignee and populates assignee_id if content contains /assign' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(assignee_id: user.id)
+ end
+ end
+
+ shared_examples 'unassign command' do
+ it 'populates assignee_id: nil if content contains /unassign' do
+ issuable.update(assignee_id: user.id)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(assignee_id: nil)
+ end
+ end
+
+ shared_examples 'milestone command' do
+ it 'fetches milestone and populates milestone_id if content contains /milestone' do
+ milestone # populate the milestone
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(milestone_id: milestone.id)
+ end
+ end
+
+ shared_examples 'remove_milestone command' do
+ it 'populates milestone_id: nil if content contains /remove_milestone' do
+ issuable.update(milestone_id: milestone.id)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(milestone_id: nil)
+ end
+ end
+
+ shared_examples 'label command' do
+ it 'fetches label ids and populates add_label_ids if content contains /label' do
+ bug # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [bug.id, inprogress.id])
+ end
+ end
+
+ shared_examples 'unlabel command' do
+ it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
+ issuable.update(label_ids: [inprogress.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(remove_label_ids: [inprogress.id])
+ end
+ end
+
+ shared_examples 'unlabel command with no argument' do
+ it 'populates label_ids: [] if content contains /unlabel with no arguments' do
+ issuable.update(label_ids: [inprogress.id]) # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(label_ids: [])
+ end
+ end
+
+ shared_examples 'relabel command' do
+ it 'populates label_ids: [] if content contains /relabel' do
+ issuable.update(label_ids: [bug.id]) # populate the label
+ inprogress # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(label_ids: [inprogress.id])
+ end
+ end
+
+ shared_examples 'todo command' do
+ it 'populates todo_event: "add" if content contains /todo' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(todo_event: 'add')
+ end
+ end
+
+ shared_examples 'done command' do
+ it 'populates todo_event: "done" if content contains /done' do
+ TodoService.new.mark_todo(issuable, user)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(todo_event: 'done')
+ end
+ end
+
+ shared_examples 'subscribe command' do
+ it 'populates subscription_event: "subscribe" if content contains /subscribe' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(subscription_event: 'subscribe')
+ end
+ end
+
+ shared_examples 'unsubscribe command' do
+ it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
+ issuable.subscribe(user)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(subscription_event: 'unsubscribe')
+ end
+ end
+
+ shared_examples 'due command' do
+ it 'populates due_date: Date.new(2016, 8, 28) if content contains /due 2016-08-28' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(due_date: defined?(expected_date) ? expected_date : Date.new(2016, 8, 28))
+ end
+ end
+
+ shared_examples 'remove_due_date command' do
+ it 'populates due_date: nil if content contains /remove_due_date' do
+ issuable.update(due_date: Date.today)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(due_date: nil)
+ end
+ end
+
+ shared_examples 'empty command' do
+ it 'populates {} if content contains an unsupported command' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to be_empty
+ end
+ end
+
+ it_behaves_like 'reopen command' do
+ let(:content) { '/reopen' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'reopen command' do
+ let(:content) { '/reopen' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'close command' do
+ let(:content) { '/close' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'close command' do
+ let(:content) { '/close' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'title command' do
+ let(:content) { '/title A brand new title' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'title command' do
+ let(:content) { '/title A brand new title' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/title' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'assign command' do
+ let(:content) { "/assign @#{user.username}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'assign command' do
+ let(:content) { "/assign @#{user.username}" }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/assign @abcd1234' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/assign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unassign command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unassign command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'milestone command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'milestone command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'remove_milestone command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'remove_milestone command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'label command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unlabel command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unlabel command with no argument' do
+ let(:content) { %(/unlabel) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unlabel command with no argument' do
+ let(:content) { %(/unlabel) }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'relabel command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'relabel command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'todo command' do
+ let(:content) { '/todo' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'todo command' do
+ let(:content) { '/todo' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'done command' do
+ let(:content) { '/done' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'done command' do
+ let(:content) { '/done' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'subscribe command' do
+ let(:content) { '/subscribe' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'subscribe command' do
+ let(:content) { '/subscribe' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unsubscribe command' do
+ let(:content) { '/unsubscribe' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'unsubscribe command' do
+ let(:content) { '/unsubscribe' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due 2016-08-28' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due tomorrow' }
+ let(:issuable) { issue }
+ let(:expected_date) { Date.tomorrow }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due 5 days from now' }
+ let(:issuable) { issue }
+ let(:expected_date) { 5.days.from_now.to_date }
+ end
+
+ it_behaves_like 'due command' do
+ let(:content) { '/due in 2 days' }
+ let(:issuable) { issue }
+ let(:expected_date) { 2.days.from_now.to_date }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due foo bar' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due 2016-08-28' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'remove_due_date command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { merge_request }
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 43693441450..3d854a959f3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -330,13 +330,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
@@ -346,13 +346,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
@@ -362,7 +362,7 @@ 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 falsey when text does not begin with expected text' do
@@ -471,15 +471,15 @@ describe SystemNoteService, services: true do
shared_examples 'cross project mentionable' do
include GitlabMarkdownHelper
- it 'should contain cross reference to new noteable' do
+ it 'contains cross reference to new noteable' do
expect(subject.note).to include cross_project_reference(new_project, new_noteable)
end
- it 'should mention referenced noteable' do
+ it 'mentions referenced noteable' do
expect(subject.note).to include new_noteable.to_reference
end
- it 'should mention referenced project' do
+ it 'mentions referenced project' do
expect(subject.note).to include new_project.to_reference
end
end
@@ -489,7 +489,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
- it 'should notify about noteable being moved to' do
+ it 'notifies about noteable being moved to' do
expect(subject.note).to match /Moved to/
end
end
@@ -499,7 +499,7 @@ describe SystemNoteService, services: true do
it_behaves_like 'cross project mentionable'
- it 'should notify about noteable being moved from' do
+ it 'notifies about noteable being moved from' do
expect(subject.note).to match /Moved from/
end
end
@@ -507,7 +507,7 @@ describe SystemNoteService, services: true do
context 'invalid direction' do
let(:direction) { :invalid }
- it 'should raise error' do
+ it 'raises error' do
expect { subject }.to raise_error StandardError, /Invalid direction/
end
end
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index f034f251ba4..4f6dd8c6d3f 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -5,8 +5,8 @@ describe TestHookService, services: true do
let(:project) { create :project }
let(:hook) { create :project_hook, project: project }
- describe :execute do
- it "should execute successfully" do
+ describe '#execute' do
+ it "executes successfully" do
stub_request(:post, hook.url).to_return(status: 200)
expect(TestHookService.new.execute(hook, user)).to be_truthy
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index b4522536724..cafcad3e3c0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -35,8 +35,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.new_issue(unassigned_issue, author) }
end
- it 'does not create a todo if assignee is the current user' do
- should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) }
+ it 'creates a todo if assignee is the current user' do
+ unassigned_issue.update_attribute(:assignee, john_doe)
+ service.new_issue(unassigned_issue, john_doe)
+
+ should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
end
it 'creates a todo for each valid mentioned user' do
@@ -44,7 +47,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
@@ -57,7 +60,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
- should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
context 'when a private group is mentioned' do
@@ -87,7 +90,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED)
+ should_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
end
@@ -105,7 +108,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
- should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED)
end
context 'issues with a task list' do
@@ -156,10 +159,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.reassigned_issue(issue, author) }
end
- it 'does not create a todo if new assignee is the current user' do
+ it 'creates a todo if new assignee is the current user' do
unassigned_issue.update_attribute(:assignee, john_doe)
+ service.reassigned_issue(unassigned_issue, john_doe)
- should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) }
+ should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
end
end
@@ -190,12 +194,12 @@ describe TodoService, services: true do
end
end
- describe '#mark_todos_as_done' do
- it 'marks related todos for the user as done' do
- first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
- second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
+ shared_examples 'marking todos as done' do |meth|
+ let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
+ let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
- service.mark_todos_as_done([first_todo, second_todo], john_doe)
+ it 'marks related todos for the user as done' do
+ service.send(meth, collection, john_doe)
expect(first_todo.reload).to be_done
expect(second_todo.reload).to be_done
@@ -203,20 +207,30 @@ describe TodoService, services: true do
describe 'cached counts' do
it 'updates when todos change' do
- todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
-
expect(john_doe.todos_done_count).to eq(0)
- expect(john_doe.todos_pending_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(2)
expect(john_doe).to receive(:update_todos_count_cache).and_call_original
- service.mark_todos_as_done([todo], john_doe)
+ service.send(meth, collection, john_doe)
- expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_done_count).to eq(2)
expect(john_doe.todos_pending_count).to eq(0)
end
end
end
+ describe '#mark_todos_as_done' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done do
+ let(:collection) { [first_todo, second_todo] }
+ end
+ end
+
+ describe '#mark_todos_as_done_by_ids' do
+ it_behaves_like 'marking todos as done', :mark_todos_as_done_by_ids do
+ let(:collection) { [first_todo, second_todo].map(&:id) }
+ end
+ end
+
describe '#new_note' do
let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) }
@@ -250,7 +264,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: guest, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
- should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
+ should_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
should_not_create_todo(user: non_member, target: issue, author: john_doe, action: Todo::MENTIONED, note: note)
end
@@ -262,7 +276,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_create_todo(user: admin, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
should_not_create_todo(user: guest, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
- should_not_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
+ should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::MENTIONED, note: note_on_confidential_issue)
end
it 'creates a todo for each valid mentioned user when leaving a note on commit' do
@@ -270,7 +284,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_not_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
end
@@ -286,6 +300,18 @@ describe TodoService, services: true do
should_create_todo(user: author, target: unassigned_issue, action: Todo::MARKED)
end
end
+
+ describe '#todo_exists?' do
+ it 'returns false when no todo exist for the given issuable' do
+ expect(service.todo_exist?(unassigned_issue, author)).to be_falsy
+ end
+
+ it 'returns true when a todo exist for the given issuable' do
+ service.mark_todo(unassigned_issue, author)
+
+ expect(service.todo_exist?(unassigned_issue, author)).to be_truthy
+ end
+ end
end
describe 'Merge Requests' do
@@ -312,7 +338,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
@@ -325,7 +351,7 @@ describe TodoService, services: true do
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
- should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
+ should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
end
@@ -382,10 +408,11 @@ describe TodoService, services: true do
should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
end
- it 'does not create a todo if new assignee is the current user' do
+ it 'creates a todo if new assignee is the current user' do
mr_assigned.update_attribute(:assignee, john_doe)
+ service.reassigned_merge_request(mr_assigned, john_doe)
- should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) }
+ should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
end
end
@@ -435,6 +462,24 @@ describe TodoService, services: true do
should_create_todo(user: author, target: mr_unassigned, action: Todo::MARKED)
end
end
+
+ describe '#new_note' do
+ let(:mention) { john_doe.to_reference }
+ let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+ let(:legacy_diff_note_on_merge_request) { create(:legacy_diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
+
+ it 'creates a todo for mentioned user on new diff note' do
+ service.new_note(diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: diff_note_on_merge_request)
+ end
+
+ it 'creates a todo for mentioned user on legacy diff note' do
+ service.new_note(legacy_diff_note_on_merge_request, author)
+
+ should_create_todo(user: john_doe, target: mr_unassigned, author: author, action: Todo::MENTIONED, note: legacy_diff_note_on_merge_request)
+ end
+ end
end
it 'updates cached counts when a todo is created' do
@@ -449,6 +494,63 @@ describe TodoService, services: true do
expect(john_doe.todos_pending_count).to eq(1)
end
+ describe '#mark_todos_as_done' do
+ let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+ let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+
+ it 'marks a relation of todos as done' do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ todos = TodosFinder.new(john_doe, {}).execute
+ expect { TodoService.new.mark_todos_as_done(todos, john_doe) }
+ .to change { john_doe.todos.done.count }.from(0).to(1)
+ end
+
+ it 'marks an array of todos as done' do
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ expect { TodoService.new.mark_todos_as_done([todo], john_doe) }
+ .to change { todo.reload.state }.from('pending').to('done')
+ end
+
+ it 'returns the number of updated todos' do # Needed on API
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+
+ expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+ end
+
+ context 'when some of the todos are done already' do
+ before do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
+ end
+
+ it 'returns the number of those still pending' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ end
+
+ it 'returns 0 if all are done' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+ TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ end
+ end
+
+ it 'caches the number of todos of a user', :caching do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ TodoService.new.mark_todos_as_done([todo], john_doe)
+
+ expect_any_instance_of(TodosFinder).not_to receive(:execute)
+
+ expect(john_doe.todos_done_count).to eq(1)
+ expect(john_doe.todos_pending_count).to eq(1)
+ end
+ end
+
def should_create_todo(attributes = {})
attributes.reverse_merge!(
project: project,
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
new file mode 100644
index 00000000000..b507d38f472
--- /dev/null
+++ b/spec/simplecov_env.rb
@@ -0,0 +1,55 @@
+require 'simplecov'
+require 'active_support/core_ext/numeric/time'
+
+module SimpleCovEnv
+ extend self
+
+ def start!
+ return unless ENV['SIMPLECOV']
+
+ configure_profile
+ configure_job
+
+ SimpleCov.start
+ end
+
+ def configure_job
+ SimpleCov.configure do
+ if ENV['CI_BUILD_NAME']
+ coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}"
+ command_name ENV['CI_BUILD_NAME']
+ end
+
+ if ENV['CI']
+ SimpleCov.at_exit do
+ # In CI environment don't generate formatted reports
+ # Only generate .resultset.json
+ SimpleCov.result
+ end
+ end
+ end
+ end
+
+ def configure_profile
+ SimpleCov.configure do
+ load_profile 'test_frameworks'
+ track_files '{app,lib}/**/*.rb'
+
+ add_filter '/vendor/ruby/'
+ add_filter 'config/initializers/'
+
+ add_group 'Controllers', 'app/controllers'
+ add_group 'Models', 'app/models'
+ add_group 'Mailers', 'app/mailers'
+ add_group 'Helpers', 'app/helpers'
+ add_group 'Workers', %w(app/jobs app/workers)
+ add_group 'Libraries', 'lib'
+ add_group 'Services', 'app/services'
+ add_group 'Finders', 'app/finders'
+ add_group 'Uploaders', 'app/uploaders'
+ add_group 'Validators', 'app/validators'
+
+ merge_timeout 365.days
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 606da1b7605..c144cd85487 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,7 +1,5 @@
-if ENV['SIMPLECOV']
- require 'simplecov'
- SimpleCov.start :rails
-end
+require './spec/simplecov_env'
+SimpleCovEnv.start!
ENV["RAILS_ENV"] ||= 'test'
@@ -33,9 +31,9 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :request
config.include StubConfiguration
config.include EmailHelpers
- config.include RelativeUrl, type: feature
config.include TestEnv
config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
@@ -45,6 +43,13 @@ RSpec.configure do |config|
config.before(:suite) do
TestEnv.init
end
+
+ config.around(:each, :caching) do |example|
+ caching_store = Rails.cache
+ Rails.cache = ActiveSupport::Cache::MemoryStore.new if example.metadata[:caching]
+ example.run
+ Rails.cache = caching_store
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/api/members_shared_examples.rb b/spec/support/api/members_shared_examples.rb
new file mode 100644
index 00000000000..dab71a35a55
--- /dev/null
+++ b/spec/support/api/members_shared_examples.rb
@@ -0,0 +1,11 @@
+shared_examples 'a 404 response when source is private' do
+ before do
+ source.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'returns 404' do
+ route
+
+ expect(response).to have_http_status(404)
+ end
+end
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
new file mode 100644
index 00000000000..e42d727672b
--- /dev/null
+++ b/spec/support/api/schema_matcher.rb
@@ -0,0 +1,8 @@
+RSpec::Matchers.define :match_response_schema do |schema, **options|
+ match do |response|
+ schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas"
+ schema_path = "#{schema_directory}/#{schema}.json"
+
+ JSON::Validator.validate!(schema_path, response.body, options)
+ end
+end
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index 1b3cafb497c..68b196d9033 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -24,8 +24,11 @@ module ApiHelpers
(path.index('?') ? '' : '?') +
# Append private_token if given a User object
- (user.respond_to?(:private_token) ?
- "&private_token=#{user.private_token}" : "")
+ if user.respond_to?(:private_token)
+ "&private_token=#{user.private_token}"
+ else
+ ''
+ end
end
def ci_api(path, user = nil)
@@ -35,8 +38,11 @@ module ApiHelpers
(path.index('?') ? '' : '?') +
# Append private_token if given a User object
- (user.respond_to?(:private_token) ?
- "&private_token=#{user.private_token}" : "")
+ if user.respond_to?(:private_token)
+ "&private_token=#{user.private_token}"
+ else
+ ''
+ end
end
def json_response
diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb
index 9b5c3065eed..b57a3493aff 100644
--- a/spec/support/capybara_helpers.rb
+++ b/spec/support/capybara_helpers.rb
@@ -27,6 +27,14 @@ module CapybaraHelpers
end
end
end
+
+ # Refresh the page. Calling `visit current_url` doesn't seem to work consistently.
+ #
+ def refresh
+ url = current_url
+ visit 'about:blank'
+ visit url
+ end
end
RSpec.configure do |config|
diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb
index a85ab22ce36..0bfc4685532 100644
--- a/spec/support/email_helpers.rb
+++ b/spec/support/email_helpers.rb
@@ -3,6 +3,16 @@ module EmailHelpers
ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
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)
+ expect(recipients.count).to eq(users.count)
+ end
+
def should_email(user)
expect(sent_to_user?(user)).to be_truthy
end
diff --git a/spec/support/fake_u2f_device.rb b/spec/support/fake_u2f_device.rb
index 553fe9f1fbc..8c407b867fe 100644
--- a/spec/support/fake_u2f_device.rb
+++ b/spec/support/fake_u2f_device.rb
@@ -1,6 +1,9 @@
class FakeU2fDevice
- def initialize(page)
+ attr_reader :name
+
+ def initialize(page, name)
@page = page
+ @name = name
end
def respond_to_u2f_registration
@@ -18,8 +21,8 @@ class FakeU2fDevice
def respond_to_u2f_authentication
app_id = @page.evaluate_script('gon.u2f.app_id')
- challenges = @page.evaluate_script('gon.u2f.challenges')
- json_response = u2f_device(app_id).sign_response(challenges[0])
+ challenge = @page.evaluate_script('gon.u2f.challenge')
+ json_response = u2f_device(app_id).sign_response(challenge)
@page.execute_script("
u2f.sign = function(appId, challenges, signRequests, callback) {
diff --git a/spec/support/import_export/import_export.yml b/spec/support/import_export/import_export.yml
index 3ceec506401..17136dee000 100644
--- a/spec/support/import_export/import_export.yml
+++ b/spec/support/import_export/import_export.yml
@@ -7,6 +7,8 @@ project_tree:
- :merge_request_test
- commit_statuses:
- :commit
+ - project_members:
+ - :user
included_attributes:
project:
@@ -14,6 +16,8 @@ included_attributes:
- :path
merge_requests:
- :id
+ user:
+ - :email
excluded_attributes:
merge_requests:
diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/issuable_create_service_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..5f9645ed44f
--- /dev/null
+++ b/spec/support/issuable_create_service_slash_commands_shared_examples.rb
@@ -0,0 +1,83 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It can take a `default_params`.
+
+shared_examples 'new issuable record that supports slash commands' do
+ let!(:project) { create(:project) }
+ let(:user) { create(:user).tap { |u| project.team << [u, :master] } }
+ let(:assignee) { create(:user) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:labels) { create_list(:label, 3, project: project) }
+ let(:base_params) { { title: FFaker::Lorem.sentence(3) } }
+ let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
+ let(:issuable) { described_class.new(project, user, params).execute }
+
+ context 'with labels in command only' do
+ let(:example_params) do
+ {
+ description: "/label ~#{labels.first.name} ~#{labels.second.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches labels to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with labels in params and command' do
+ let(:example_params) do
+ {
+ label_ids: [labels.second.id],
+ description: "/label ~#{labels.first.name}\n/unlabel ~#{labels.third.name}"
+ }
+ end
+
+ it 'attaches all labels to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.label_ids).to match_array([labels.first.id, labels.second.id])
+ end
+ end
+
+ context 'with assignee and milestone in command only' do
+ let(:example_params) do
+ {
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ it 'assigns and sets milestone to issuable' do
+ expect(issuable).to be_persisted
+ expect(issuable.assignee).to eq(assignee)
+ expect(issuable.milestone).to eq(milestone)
+ end
+ end
+
+ context 'with assignee and milestone in params and command' do
+ let(:example_params) do
+ {
+ assignee: build_stubbed(:user),
+ milestone_id: double(:milestone),
+ description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}")
+ }
+ end
+
+ it 'assigns and sets milestone to issuable from command' do
+ expect(issuable).to be_persisted
+ expect(issuable.assignee).to eq(assignee)
+ expect(issuable.milestone).to eq(milestone)
+ end
+ end
+
+ describe '/close' do
+ let(:example_params) do
+ {
+ description: '/close'
+ }
+ end
+
+ it 'returns an open issue' do
+ expect(issuable).to be_persisted
+ expect(issuable).to be_open
+ end
+ end
+end
diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb
new file mode 100644
index 00000000000..d2a49ea5c5e
--- /dev/null
+++ b/spec/support/issuable_slash_commands_shared_examples.rb
@@ -0,0 +1,289 @@
+# Specifications for behavior common to all objects with executable attributes.
+# It takes a `issuable_type`, and expect an `issuable`.
+
+shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
+ let(:master) { create(:user) }
+ let(:assignee) { create(:user, username: 'bob') }
+ let(:guest) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+ let!(:label_bug) { create(:label, project: project, title: 'bug') }
+ let!(:label_feature) { create(:label, project: project, title: 'feature') }
+ let(:new_url_opts) { {} }
+
+ before do
+ project.team << [master, :master]
+ project.team << [assignee, :developer]
+ project.team << [guest, :guest]
+ login_with(master)
+ end
+
+ describe "new #{issuable_type}" do
+ context 'with commands in the description' do
+ it "creates the #{issuable_type} and interpret commands accordingly" do
+ visit public_send("new_namespace_project_#{issuable_type}_path", project.namespace, project, new_url_opts)
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\""
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq "bug description"
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+ end
+
+ describe "note on #{issuable_type}" do
+ before do
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ context 'with a note containing commands' do
+ it 'creates a note without the commands and interpret the commands accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+ click_button 'Comment'
+ end
+
+ expect(page).to have_content 'Awesome!'
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).not_to have_content '/label ~bug'
+ expect(page).not_to have_content '/milestone %"ASAP"'
+
+ issuable.reload
+ note = issuable.notes.user.first
+
+ expect(note.note).to eq "Awesome!"
+ expect(issuable.assignee).to eq assignee
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ end
+ end
+
+ context 'with a note containing only commands' do
+ it 'does not create a note but interpret the commands accordingly' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).not_to have_content '/label ~bug'
+ expect(page).not_to have_content '/milestone %"ASAP"'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issuable.reload
+
+ expect(issuable.notes.user).to be_empty
+ expect(issuable.assignee).to eq assignee
+ expect(issuable.labels).to eq [label_bug]
+ expect(issuable.milestone).to eq milestone
+ end
+ end
+
+ context "with a note closing the #{issuable_type}" do
+ before do
+ expect(issuable).to be_open
+ end
+
+ context "when current user can close #{issuable_type}" do
+ it "closes the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/close"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/close'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload).to be_closed
+ end
+ end
+
+ context "when current user cannot close #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not close the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/close"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/close'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable).to be_open
+ end
+ end
+ end
+
+ context "with a note reopening the #{issuable_type}" do
+ before do
+ issuable.close
+ expect(issuable).to be_closed
+ end
+
+ context "when current user can reopen #{issuable_type}" do
+ it "reopens the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/reopen"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/reopen'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload).to be_open
+ end
+ end
+
+ context "when current user cannot reopen #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not reopen the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/reopen"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/reopen'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable).to be_closed
+ end
+ end
+ end
+
+ context "with a note changing the #{issuable_type}'s title" do
+ context "when current user can change title of #{issuable_type}" do
+ it "reopens the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/title Awesome new title"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/title'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload.title).to eq 'Awesome new title'
+ end
+ end
+
+ context "when current user cannot change title of #{issuable_type}" do
+ before do
+ logout
+ login_with(guest)
+ visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
+ end
+
+ it "does not reopen the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/title Awesome new title"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/title'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(issuable.reload.title).not_to eq 'Awesome new title'
+ end
+ end
+ end
+
+ context "with a note marking the #{issuable_type} as todo" do
+ it "creates a new todo for the #{issuable_type}" do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/todo"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/todo'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ todos = TodosFinder.new(master).execute
+ todo = todos.first
+
+ expect(todos.size).to eq 1
+ expect(todo).to be_pending
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq master
+ expect(todo.user).to eq master
+ end
+ end
+
+ context "with a note marking the #{issuable_type} as done" do
+ before do
+ TodoService.new.mark_todo(issuable, master)
+ end
+
+ it "creates a new todo for the #{issuable_type}" do
+ todos = TodosFinder.new(master).execute
+ todo = todos.first
+
+ expect(todos.size).to eq 1
+ expect(todos.first).to be_pending
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq master
+ expect(todo.user).to eq master
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/done"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/done'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(todo.reload).to be_done
+ end
+ end
+
+ 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
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/subscribe"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/subscribe'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.subscribed?(master)).to be_truthy
+ end
+ end
+
+ context "with a note unsubscribing to the #{issuable_type} as done" do
+ before do
+ issuable.subscribe(master)
+ end
+
+ it "creates a new todo for the #{issuable_type}" do
+ expect(issuable.subscribed?(master)).to be_truthy
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "/unsubscribe"
+ click_button 'Comment'
+ end
+
+ expect(page).not_to have_content '/unsubscribe'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(issuable.subscribed?(master)).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb
new file mode 100644
index 00000000000..85241793743
--- /dev/null
+++ b/spec/support/issue_helpers.rb
@@ -0,0 +1,13 @@
+module IssueHelpers
+ def visit_issues(project, opts = {})
+ visit namespace_project_issues_path project.namespace, project, opts
+ end
+
+ def first_issue
+ page.all('ul.issues-list > li').first.text
+ end
+
+ def last_issue
+ page.all('ul.issues-list > li').last.text
+ end
+end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ffdf2bb0a8a..e5f76afbfc0 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -37,6 +37,40 @@ module LoginHelpers
Thread.current[:current_user] = user
end
+ def login_via(provider, user, uid)
+ mock_auth_hash(provider, uid, user.email)
+ visit new_user_session_path
+ click_link provider
+ end
+
+ def mock_auth_hash(provider, uid, email)
+ # The mock_auth configuration allows you to set per-provider (or default)
+ # authentication hashes to return during integration testing.
+ OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
+ provider: provider,
+ uid: uid,
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ },
+ credentials: {
+ token: 'mock_token',
+ secret: 'mock_secret'
+ },
+ extra: {
+ raw_info: {
+ info: {
+ name: 'mockuser',
+ email: email,
+ image: 'mock_user_thumbnail_url'
+ }
+ }
+ }
+ })
+ Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[:saml]
+ end
+
# Requires Javascript driver.
def logout
find(".header-user-dropdown-toggle").click
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index e005058ba5b..8c98b1f988c 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -178,6 +178,17 @@ module MarkdownMatchers
expect(actual).to have_selector('span.idiff.deletion', count: 2)
end
end
+
+ # VideoLinkFilter
+ matcher :parse_video_links do
+ set_default_markdown_messages
+
+ match do |actual|
+ video = actual.at_css('video')
+
+ expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb
new file mode 100644
index 00000000000..d5801c8272f
--- /dev/null
+++ b/spec/support/merge_request_helpers.rb
@@ -0,0 +1,13 @@
+module MergeRequestHelpers
+ def visit_merge_requests(project, opts = {})
+ visit namespace_project_merge_requests_path project.namespace, project, opts
+ end
+
+ def first_merge_request
+ page.all('ul.mr-list > li').first.text
+ end
+
+ def last_merge_request
+ page.all('ul.mr-list > li').last.text
+ end
+end
diff --git a/spec/support/omni_auth.rb b/spec/support/omni_auth.rb
new file mode 100644
index 00000000000..0b1af4052ff
--- /dev/null
+++ b/spec/support/omni_auth.rb
@@ -0,0 +1 @@
+OmniAuth.config.test_mode = true
diff --git a/spec/support/relative_url.rb b/spec/support/relative_url.rb
deleted file mode 100644
index 72e3ccce75b..00000000000
--- a/spec/support/relative_url.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Fix route helpers in tests (e.g. root_path, ...)
-module RelativeUrl
- extend ActiveSupport::Concern
-
- included do
- default_url_options[:script_name] = Rails.application.config.relative_url_root
- end
-end
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 04d25b5e9e9..35cc51725c6 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -11,7 +11,7 @@
#
module Select2Helper
- def select2(value, options={})
+ def select2(value, options = {})
raise ArgumentError, 'options must be a Hash' unless options.kind_of?(Hash)
selector = options.fetch(:from)
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 6b99b0f24cb..c7a45fc4ff9 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -5,19 +5,32 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
- 'empty-branch' => '7efb185',
- 'flatten-dir' => 'e56497b',
- 'feature' => '0b4bc9a',
- 'feature_conflict' => 'bb5206f',
- 'fix' => '48f0be4',
- 'improve/awesome' => '5937ac0',
- 'markdown' => '0ed8c6c',
- 'lfs' => 'be93687',
- 'master' => '5937ac0',
- "'test'" => 'e56497b',
- 'orphaned-branch' => '45127a9',
- 'binary-encoding' => '7b1cf43',
- 'gitattributes' => '5a62481',
+ 'empty-branch' => '7efb185',
+ 'ends-with.json' => '98b0d8b3',
+ 'flatten-dir' => 'e56497b',
+ 'feature' => '0b4bc9a',
+ 'feature_conflict' => 'bb5206f',
+ 'fix' => '48f0be4',
+ 'improve/awesome' => '5937ac0',
+ 'markdown' => '0ed8c6c',
+ 'lfs' => 'be93687',
+ 'master' => '5937ac0',
+ "'test'" => 'e56497b',
+ 'orphaned-branch' => '45127a9',
+ 'binary-encoding' => '7b1cf43',
+ 'gitattributes' => '5a62481',
+ 'expand-collapse-diffs' => '4842455',
+ 'expand-collapse-files' => '025db92',
+ 'expand-collapse-lines' => '238e82d',
+ 'video' => '8879059',
+ 'crlf-diff' => '5938907',
+ 'conflict-start' => '75284c7',
+ 'conflict-resolvable' => '1450cd6',
+ 'conflict-binary-file' => '259a6fb',
+ 'conflict-contains-conflict-markers' => '5e0964c',
+ 'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
+ 'conflict-too-large' => '39fa04f',
}
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/support/updating_mentions_shared_examples.rb b/spec/support/updating_mentions_shared_examples.rb
new file mode 100644
index 00000000000..e0c59a5c280
--- /dev/null
+++ b/spec/support/updating_mentions_shared_examples.rb
@@ -0,0 +1,32 @@
+RSpec.shared_examples 'updating mentions' do |service_class|
+ let(:mentioned_user) { create(:user) }
+ let(:service_class) { service_class }
+
+ before { project.team << [mentioned_user, :developer] }
+
+ def update_mentionable(opts)
+ reset_delivered_emails!
+
+ perform_enqueued_jobs do
+ service_class.new(project, user, opts).execute(mentionable)
+ end
+
+ mentionable.reload
+ end
+
+ context 'in title' do
+ before { update_mentionable(title: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+
+ context 'in description' do
+ before { update_mentionable(description: mentioned_user.to_reference) }
+
+ it 'emails only the newly-mentioned user' do
+ should_only_email(mentioned_user)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index d2c056d8e14..548e7780c36 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -42,7 +42,7 @@ describe 'gitlab:app namespace rake task' do
before do
allow(Dir).to receive(:glob).and_return([])
allow(Dir).to receive(:chdir)
- allow(File).to receive(:exists?).and_return(true)
+ allow(File).to receive(:exist?).and_return(true)
allow(Kernel).to receive(:system).and_return(true)
allow(FileUtils).to receive(:cp_r).and_return(true)
allow(FileUtils).to receive(:mv).and_return(true)
@@ -53,7 +53,7 @@ describe 'gitlab:app namespace rake task' do
let(:gitlab_version) { Gitlab::VERSION }
- it 'should fail on mismatch' do
+ it 'fails on mismatch' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: "not #{gitlab_version}" })
@@ -61,7 +61,7 @@ describe 'gitlab:app namespace rake task' do
to raise_error(SystemExit)
end
- it 'should invoke restoration on match' do
+ it 'invokes restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
@@ -107,7 +107,7 @@ describe 'gitlab:app namespace rake task' do
end
context 'archive file permissions' do
- it 'should set correct permissions on the tar file' do
+ it 'sets correct permissions on the tar file' do
expect(File.exist?(@backup_tar)).to be_truthy
expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
end
@@ -127,7 +127,7 @@ describe 'gitlab:app namespace rake task' do
end
end
- it 'should set correct permissions on the tar contents' do
+ it 'sets correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz}
)
@@ -142,7 +142,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
end
- it 'should delete temp directories' do
+ it 'deletes temp directories' do
temp_dirs = Dir.glob(
File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}')
)
@@ -153,7 +153,7 @@ describe 'gitlab:app namespace rake task' do
context 'registry disabled' do
let(:enable_registry) { false }
- it 'should not create registry.tar.gz' do
+ it 'does not create registry.tar.gz' do
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar}}
)
@@ -191,7 +191,7 @@ describe 'gitlab:app namespace rake task' do
FileUtils.rm(@backup_tar)
end
- it 'should include repositories in all repository storages' do
+ it 'includes repositories in all repository storages' do
tar_contents, exit_status = Gitlab::Popen.popen(
%W{tar -tvf #{@backup_tar} repositories}
)
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 36d03a224e4..fc52c04e78d 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -19,7 +19,7 @@ describe 'gitlab:db namespace rake task' do
end
describe 'configure' do
- it 'should invoke db:migrate when schema has already been loaded' do
+ it 'invokes db:migrate when schema has already been loaded' do
allow(ActiveRecord::Base.connection).to receive(:tables).and_return(['default'])
expect(Rake::Task['db:migrate']).to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -27,7 +27,7 @@ describe 'gitlab:db namespace rake task' do
expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
end
- it 'should invoke db:shema:load and db:seed_fu when schema is not loaded' do
+ it 'invokes db:shema:load and db:seed_fu when schema is not loaded' do
allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
expect(Rake::Task['db:schema:load']).to receive(:invoke)
expect(Rake::Task['db:seed_fu']).to receive(:invoke)
@@ -35,7 +35,7 @@ describe 'gitlab:db namespace rake task' do
expect { run_rake_task('gitlab:db:configure') }.not_to raise_error
end
- it 'should not invoke any other rake tasks during an error' do
+ it 'does not invoke any other rake tasks during an error' do
allow(ActiveRecord::Base).to receive(:connection).and_raise(RuntimeError, 'error')
expect(Rake::Task['db:migrate']).not_to receive(:invoke)
expect(Rake::Task['db:schema:load']).not_to receive(:invoke)
@@ -45,7 +45,7 @@ describe 'gitlab:db namespace rake task' do
allow(ActiveRecord::Base).to receive(:connection).and_call_original
end
- it 'should not invoke seed after a failed schema_load' do
+ it 'does not invoke seed after a failed schema_load' do
allow(ActiveRecord::Base.connection).to receive(:tables).and_return([])
allow(Rake::Task['db:schema:load']).to receive(:invoke).and_raise(RuntimeError, 'error')
expect(Rake::Task['db:schema:load']).to receive(:invoke)
diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb
index 69b2b9b6d5b..5ea020f313c 100644
--- a/spec/teaspoon_env.rb
+++ b/spec/teaspoon_env.rb
@@ -38,7 +38,7 @@ Teaspoon.configure do |config|
# Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These
# files need to be within an asset path. You can add asset paths using the `config.asset_paths`.
- suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}"
+ suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}"
# Load additional JS files, but requiring them in your spec helper is the preferred way to do this.
# suite.javascripts = []
@@ -149,7 +149,7 @@ Teaspoon.configure do |config|
# Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage
# on the CLI.
# Set this to "true" or the name of your coverage config.
- # config.use_coverage = nil
+ config.use_coverage = true
# You can have multiple coverage configs by passing a name to config.coverage.
# e.g. config.coverage :ci do |coverage|
@@ -158,15 +158,15 @@ Teaspoon.configure do |config|
# Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports.
#
# Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity
- # coverage.reports = ["text-summary", "html"]
+ coverage.reports = ["text-summary", "html"]
# The path that the coverage should be written to - when there's an artifact to write to disk.
# Note: Relative to `config.root`.
- # coverage.output_path = "coverage"
+ coverage.output_path = "coverage-javascript"
# Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The
# default excludes assets from vendor, gems and support libraries.
- # coverage.ignore = [%r{/lib/ruby/gems/}, %r{/vendor/assets/}, %r{/support/}, %r{/(.+)_helper.}]
+ coverage.ignore = [%r{vendor/}, %r{spec/}]
# Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any
# aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil.
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
new file mode 100644
index 00000000000..e8300abed5d
--- /dev/null
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe FileUploader do
+ let(:project) { create(:project) }
+
+ before do
+ @previous_enable_processing = FileUploader.enable_processing
+ FileUploader.enable_processing = false
+ @uploader = FileUploader.new(project)
+ end
+
+ after do
+ FileUploader.enable_processing = @previous_enable_processing
+ @uploader.remove!
+ end
+
+ describe '#image_or_video?' do
+ context 'given an image file' do
+ before do
+ @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')))
+ end
+
+ it 'detects an image based on file extension' do
+ expect(@uploader.image_or_video?).to be true
+ end
+ end
+
+ context 'given an video file' do
+ before do
+ video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4'))
+ @uploader.store!(video_file)
+ end
+
+ it 'detects a video based on file extension' do
+ expect(@uploader.image_or_video?).to be true
+ end
+ end
+
+ it 'does not return image_or_video? for other types' do
+ @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt')))
+
+ expect(@uploader.image_or_video?).to be false
+ end
+ end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
new file mode 100644
index 00000000000..dae858a52f6
--- /dev/null
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'admin/dashboard/index.html.haml' do
+ include Devise::TestHelpers
+
+ before do
+ assign(:projects, create_list(:empty_project, 1))
+ assign(:users, create_list(:user, 1))
+ assign(:groups, create_list(:group, 1))
+
+ allow(view).to receive(:admin?).and_return(true)
+ end
+
+ it "shows version of GitLab Workhorse" do
+ render
+
+ expect(rendered).to have_content 'GitLab Workhorse'
+ expect(rendered).to have_content Gitlab::Workhorse.version
+ end
+end
diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
index 05a76ee4bdb..ee362e6fcb3 100644
--- a/spec/views/devise/shared/_signin_box.html.haml_spec.rb
+++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'devise/shared/_signin_box' do
def enable_crowd
allow(view).to receive(:form_based_providers).and_return([:crowd])
allow(view).to receive(:crowd_enabled?).and_return(true)
- allow(view).to receive(:user_omniauth_authorize_path).with('crowd').
+ allow(view).to receive(:omniauth_authorize_path).with(:user, :crowd).
and_return('/crowd')
end
end
diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb
new file mode 100644
index 00000000000..3fddfb3b62f
--- /dev/null
+++ b/spec/views/layouts/_head.html.haml_spec.rb
@@ -0,0 +1,36 @@
+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)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ it 'escapes HTML-safe strings in page_description' do
+ stub_helper_with_safe_string(:page_description)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ it 'escapes HTML-safe strings in page_image' do
+ stub_helper_with_safe_string(:page_image)
+
+ render
+
+ expect(rendered).to match(%{content="foo&quot; http-equiv=&quot;refresh"})
+ end
+
+ def stub_helper_with_safe_string(method)
+ allow_any_instance_of(PageLayoutHelper).to receive(method)
+ .and_return(%q{foo" http-equiv="refresh}.html_safe)
+ 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 cd18d19ef5e..464051063d8 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -3,8 +3,12 @@ require 'spec_helper'
describe 'projects/builds/show' do
include Devise::TestHelpers
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
+ let(:project) { create(:project) }
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id)
+ end
+ let(:build) { create(:ci_build, pipeline: pipeline) }
before do
assign(:build, build)
@@ -34,4 +38,35 @@ describe 'projects/builds/show' do
expect(rendered).to have_link('Retry')
end
end
+
+ describe 'commit title in sidebar' do
+ let(:commit_title) { project.commit.title }
+
+ it 'shows commit title and not show commit message' do
+ render
+
+ expect(rendered).to have_css('p.build-light-text.append-bottom-0',
+ text: /\A\n#{Regexp.escape(commit_title)}\n\Z/)
+ end
+ end
+
+ describe 'shows trigger variables in sidebar' do
+ let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) }
+
+ before do
+ build.trigger_request = trigger_request
+ render
+ end
+
+ it 'shows trigger variables in separate lines' do
+ expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1'))
+ expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2'))
+ end
+ end
+
+ private
+
+ def variable_regexp(key, value)
+ /\A#{Regexp.escape("#{key}=#{value}")}\Z/
+ end
end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
new file mode 100644
index 00000000000..78af61f15a7
--- /dev/null
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'projects/issues/_related_branches' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:branch) { project.repository.find_branch('feature') }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') }
+
+ before do
+ assign(:project, project)
+ assign(:related_branches, ['feature'])
+
+ render
+ end
+
+ it 'shows the related branches with their build status' do
+ expect(rendered).to match('feature')
+ expect(rendered).to have_css('.related-branch-ci-status')
+ end
+end
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
new file mode 100644
index 00000000000..733b2dfa7ff
--- /dev/null
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/widget/_heading' do
+ include Devise::TestHelpers
+
+ context 'when released to an environment' do
+ let(:project) { merge_request.target_project }
+ let(:merge_request) { create(:merge_request, :merged) }
+ let(:environment) { create(:environment, project: project) }
+ let!(:deployment) do
+ create(:deployment, environment: environment, sha: project.commit('master').id)
+ end
+
+ before do
+ assign(:merge_request, merge_request)
+ assign(:project, project)
+
+ render
+ end
+
+ it 'displays that the environment is deployed' do
+ expect(rendered).to match("Deployed to")
+ expect(rendered).to match("#{environment.name}")
+ end
+ end
+end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
new file mode 100644
index 00000000000..0f3fc1ee1ac
--- /dev/null
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe 'projects/tree/show' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ before do
+ assign(:project, project)
+ assign(:repository, repository)
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ context 'for branch names ending on .json' do
+ let(:ref) { 'ends-with.json' }
+ let(:commit) { repository.commit(ref) }
+ let(:path) { '' }
+ let(:tree) { repository.tree(commit.id, path) }
+
+ before do
+ assign(:ref, ref)
+ assign(:commit, commit)
+ assign(:id, commit.id)
+ assign(:tree, tree)
+ assign(:path, path)
+ end
+
+ it 'displays correctly' do
+ render
+ expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref)
+ expect(rendered).to have_css('.readme-holder .file-content', text: ref)
+ end
+ end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
index 98deae0a588..788b92c1b84 100644
--- a/spec/workers/build_email_worker_spec.rb
+++ b/spec/workers/build_email_worker_spec.rb
@@ -5,7 +5,7 @@ describe BuildEmailWorker do
let(:build) { create(:ci_build) }
let(:user) { create(:user) }
- let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build) }
subject { BuildEmailWorker.new }
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index de40a6f78af..fe70501eeac 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -17,7 +17,7 @@ describe EmailReceiverWorker do
context "when an error occurs" do
before do
- allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError)
+ allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError)
end
it "sends out a rejection email" do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 439da765c2c..7ca2c29da1c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,25 +2,84 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+ let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+ let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
- context "when there are no errors in sending" do
- let(:email) { ActionMailer::Base.deliveries.last }
+ context "when push is a new branch" do
+ before do
+ data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
+
+ subject.perform(project.id, recipients, data_new_branch)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include("Pushed new branch")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when push is a deleted branch" do
+ before do
+ data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
+
+ subject.perform(project.id, recipients, data_deleted_branch)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include("Deleted branch")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when push is a force push to delete commits" do
+ before do
+ data_force_push = data.stringify_keys.merge(
+ "after" => data[:before],
+ "before" => data[:after]
+ )
+
+ subject.perform(project.id, recipients, data_force_push)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+ it "mentions force pushing in the body" do
+ expect(email).to have_body_text("force push")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
+ it "does not mention force pushing in the body" do
+ expect(email).not_to have_body_text("force push")
+ end
+
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
@@ -30,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(subject).to receive_message_chain(:logger, :info)
perform
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
new file mode 100644
index 00000000000..c9f5aae0815
--- /dev/null
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe GitGarbageCollectWorker do
+ let(:project) { create(:project) }
+ let(:shell) { Gitlab::Shell.new }
+
+ subject { GitGarbageCollectWorker.new }
+
+ before do
+ allow(subject).to receive(:gitlab_shell).and_return(shell)
+ end
+
+ describe "#perform" do
+ it "runs `git gc`" do
+ expect(shell).to receive(:gc).with(
+ project.repository_storage_path,
+ project.path_with_namespace).
+ and_return(true)
+ expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
new file mode 100644
index 00000000000..4e4eaf9b2f7
--- /dev/null
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe GroupDestroyWorker do
+ let(:group) { create(:group) }
+ let(:user) { create(:admin) }
+ let!(:project) { create(:project, namespace: group) }
+
+ subject { GroupDestroyWorker.new }
+
+ describe "#perform" do
+ it "deletes the project" do
+ subject.perform(group.id, user.id)
+
+ expect(Group.all).not_to include(group)
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(project.path)).to be_falsey
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 20b1a343c27..1d2cf7acddd 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -22,7 +22,7 @@ describe PostReceive do
context "branches" do
let(:changes) { "123456 789012 refs/heads/tést" }
- it "should call GitTagPushService" do
+ it "calls GitTagPushService" do
expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -32,7 +32,7 @@ describe PostReceive do
context "tags" do
let(:changes) { "123456 789012 refs/tags/tag" }
- it "should call GitTagPushService" do
+ it "calls GitTagPushService" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -42,7 +42,7 @@ describe PostReceive do
context "merge-requests" do
let(:changes) { "123456 789012 refs/merge-requests/123" }
- it "should not call any of the services" do
+ it "does not call any of the services" do
expect_any_instance_of(GitPushService).not_to receive(:execute)
expect_any_instance_of(GitTagPushService).not_to receive(:execute)
PostReceive.new.perform(pwd(project), key_id, base64_changes)
@@ -53,7 +53,13 @@ describe PostReceive do
subject { PostReceive.new.perform(pwd(project), key_id, base64_changes) }
context "creates a Ci::Pipeline for every change" do
- before { stub_ci_pipeline_to_return_yaml_file }
+ before do
+ allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
+ OpenStruct.new(id: '123456')
+ end
+ allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
+ stub_ci_pipeline_to_return_yaml_file
+ end
it { expect{ subject }.to change{ Ci::Pipeline.count }.by(2) }
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
new file mode 100644
index 00000000000..1b910d9b91e
--- /dev/null
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe ProjectDestroyWorker do
+ let(:project) { create(:project) }
+ let(:path) { project.repository.path_to_repo }
+
+ subject { ProjectDestroyWorker.new }
+
+ describe "#perform" do
+ it "deletes the project" do
+ subject.perform(project.id, project.owner, {})
+
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ end
+
+ it "deletes the project but skips repo deletion" do
+ subject.perform(project.id, project.owner, { "skip_repo" => true })
+
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_truthy
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 00000000000..689bc3d27b4
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+ describe '#perform' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 00000000000..402aa1e714e
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+ let(:worker) { RemoveExpiredMembersWorker.new }
+
+ describe '#perform' do
+ context 'project members' do
+ let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_project_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(project_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_project_member.reload).to be_present
+ end
+ end
+
+ context 'group members' do
+ let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_group_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(group_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_group_member.reload).to be_present
+ end
+ end
+
+ context 'when the last group owner expires' do
+ let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+ it 'does not delete the owner' do
+ worker.perform
+ expect(expired_group_owner.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 5f762282b5e..60605460adb 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -14,21 +14,24 @@ describe RepositoryForkWorker do
describe "#perform" do
it "creates a new repository from a fork" do
expect(shell).to receive(:fork_repository).with(
- project.repository_storage_path,
+ '/test/path',
project.path_with_namespace,
+ project.repository_storage_path,
fork_project.namespace.path
).and_return(true)
subject.perform(
project.id,
+ '/test/path',
project.path_with_namespace,
fork_project.namespace.path)
end
it 'flushes various caches' do
expect(shell).to receive(:fork_repository).with(
- project.repository_storage_path,
+ '/test/path',
project.path_with_namespace,
+ project.repository_storage_path,
fork_project.namespace.path
).and_return(true)
@@ -38,7 +41,7 @@ describe RepositoryForkWorker do
expect_any_instance_of(Repository).to receive(:expire_exists_cache).
and_call_original
- subject.perform(project.id, project.path_with_namespace,
+ subject.perform(project.id, '/test/path', project.path_with_namespace,
fork_project.namespace.path)
end
@@ -49,6 +52,7 @@ describe RepositoryForkWorker do
subject.perform(
project.id,
+ '/test/path',
project.path_with_namespace,
fork_project.namespace.path)
end
diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js
index c264262ba73..c264262ba73 100755..100644
--- a/vendor/assets/javascripts/Chart.js
+++ b/vendor/assets/javascripts/Chart.js
diff --git a/vendor/assets/javascripts/Sortable.js b/vendor/assets/javascripts/Sortable.js
new file mode 100644
index 00000000000..eca7c5012b2
--- /dev/null
+++ b/vendor/assets/javascripts/Sortable.js
@@ -0,0 +1,1285 @@
+/**!
+ * Sortable
+ * @author RubaXa <trash@rubaxa.org>
+ * @license MIT
+ */
+
+
+(function (factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ }
+ else if (typeof module != "undefined" && typeof module.exports != "undefined") {
+ module.exports = factory();
+ }
+ else if (typeof Package !== "undefined") {
+ Sortable = factory(); // export for Meteor.js
+ }
+ else {
+ /* jshint sub:true */
+ window["Sortable"] = factory();
+ }
+})(function () {
+ "use strict";
+
+ var dragEl,
+ parentEl,
+ ghostEl,
+ cloneEl,
+ rootEl,
+ nextEl,
+
+ scrollEl,
+ scrollParentEl,
+
+ lastEl,
+ lastCSS,
+ lastParentCSS,
+
+ oldIndex,
+ newIndex,
+
+ activeGroup,
+ autoScroll = {},
+
+ tapEvt,
+ touchEvt,
+
+ moved,
+
+ /** @const */
+ RSPACE = /\s+/g,
+
+ expando = 'Sortable' + (new Date).getTime(),
+
+ win = window,
+ document = win.document,
+ parseInt = win.parseInt,
+
+ supportDraggable = !!('draggable' in document.createElement('div')),
+ supportCssPointerEvents = (function (el) {
+ el = document.createElement('x');
+ el.style.cssText = 'pointer-events:auto';
+ return el.style.pointerEvents === 'auto';
+ })(),
+
+ _silent = false,
+
+ abs = Math.abs,
+ min = Math.min,
+ slice = [].slice,
+
+ touchDragOverListeners = [],
+
+ _autoScroll = _throttle(function (/**Event*/evt, /**Object*/options, /**HTMLElement*/rootEl) {
+ // Bug: https://bugzilla.mozilla.org/show_bug.cgi?id=505521
+ if (rootEl && options.scroll) {
+ var el,
+ rect,
+ sens = options.scrollSensitivity,
+ speed = options.scrollSpeed,
+
+ x = evt.clientX,
+ y = evt.clientY,
+
+ winWidth = window.innerWidth,
+ winHeight = window.innerHeight,
+
+ vx,
+ vy
+ ;
+
+ // Delect scrollEl
+ if (scrollParentEl !== rootEl) {
+ scrollEl = options.scroll;
+ scrollParentEl = rootEl;
+
+ if (scrollEl === true) {
+ scrollEl = rootEl;
+
+ do {
+ if ((scrollEl.offsetWidth < scrollEl.scrollWidth) ||
+ (scrollEl.offsetHeight < scrollEl.scrollHeight)
+ ) {
+ break;
+ }
+ /* jshint boss:true */
+ } while (scrollEl = scrollEl.parentNode);
+ }
+ }
+
+ if (scrollEl) {
+ el = scrollEl;
+ rect = scrollEl.getBoundingClientRect();
+ vx = (abs(rect.right - x) <= sens) - (abs(rect.left - x) <= sens);
+ vy = (abs(rect.bottom - y) <= sens) - (abs(rect.top - y) <= sens);
+ }
+
+
+ if (!(vx || vy)) {
+ vx = (winWidth - x <= sens) - (x <= sens);
+ vy = (winHeight - y <= sens) - (y <= sens);
+
+ /* jshint expr:true */
+ (vx || vy) && (el = win);
+ }
+
+
+ if (autoScroll.vx !== vx || autoScroll.vy !== vy || autoScroll.el !== el) {
+ autoScroll.el = el;
+ autoScroll.vx = vx;
+ autoScroll.vy = vy;
+
+ clearInterval(autoScroll.pid);
+
+ if (el) {
+ autoScroll.pid = setInterval(function () {
+ if (el === win) {
+ win.scrollTo(win.pageXOffset + vx * speed, win.pageYOffset + vy * speed);
+ } else {
+ vy && (el.scrollTop += vy * speed);
+ vx && (el.scrollLeft += vx * speed);
+ }
+ }, 24);
+ }
+ }
+ }
+ }, 30),
+
+ _prepareGroup = function (options) {
+ var group = options.group;
+
+ if (!group || typeof group != 'object') {
+ group = options.group = {name: group};
+ }
+
+ ['pull', 'put'].forEach(function (key) {
+ if (!(key in group)) {
+ group[key] = true;
+ }
+ });
+
+ options.groups = ' ' + group.name + (group.put.join ? ' ' + group.put.join(' ') : '') + ' ';
+ }
+ ;
+
+
+
+ /**
+ * @class Sortable
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ function Sortable(el, options) {
+ if (!(el && el.nodeType && el.nodeType === 1)) {
+ throw 'Sortable: `el` must be HTMLElement, and not ' + {}.toString.call(el);
+ }
+
+ this.el = el; // root element
+ this.options = options = _extend({}, options);
+
+
+ // Export instance
+ el[expando] = this;
+
+
+ // Default options
+ var defaults = {
+ group: Math.random(),
+ sort: true,
+ disabled: false,
+ store: null,
+ handle: null,
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ draggable: /[uo]l/i.test(el.nodeName) ? 'li' : '>*',
+ ghostClass: 'sortable-ghost',
+ chosenClass: 'sortable-chosen',
+ ignore: 'a, img',
+ filter: null,
+ animation: 0,
+ setData: function (dataTransfer, dragEl) {
+ dataTransfer.setData('Text', dragEl.textContent);
+ },
+ dropBubble: false,
+ dragoverBubble: false,
+ dataIdAttr: 'data-id',
+ delay: 0,
+ forceFallback: false,
+ fallbackClass: 'sortable-fallback',
+ fallbackOnBody: false,
+ fallbackTolerance: 0
+ };
+
+
+ // Set default options
+ for (var name in defaults) {
+ !(name in options) && (options[name] = defaults[name]);
+ }
+
+ _prepareGroup(options);
+
+ // Bind all private methods
+ for (var fn in this) {
+ if (fn.charAt(0) === '_') {
+ this[fn] = this[fn].bind(this);
+ }
+ }
+
+ // Setup drag mode
+ this.nativeDraggable = options.forceFallback ? false : supportDraggable;
+
+ // Bind events
+ _on(el, 'mousedown', this._onTapStart);
+ _on(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _on(el, 'dragover', this);
+ _on(el, 'dragenter', this);
+ }
+
+ touchDragOverListeners.push(this._onDragOver);
+
+ // Restore sorting
+ options.store && this.sort(options.store.get(this));
+ }
+
+
+ Sortable.prototype = /** @lends Sortable.prototype */ {
+ constructor: Sortable,
+
+ _onTapStart: function (/** Event|TouchEvent */evt) {
+ var _this = this,
+ el = this.el,
+ options = this.options,
+ type = evt.type,
+ touch = evt.touches && evt.touches[0],
+ target = (touch || evt).target,
+ originalTarget = target,
+ filter = options.filter,
+ startIndex;
+
+ // Don't trigger start event when an element is been dragged, otherwise the evt.oldindex always wrong when set option.group.
+ if (dragEl) {
+ return;
+ }
+
+ if (type === 'mousedown' && evt.button !== 0 || options.disabled) {
+ return; // only left button or enabled
+ }
+
+ target = _closest(target, options.draggable, el);
+
+ if (!target) {
+ return;
+ }
+
+ if (options.handle && !_closest(originalTarget, options.handle, el)) {
+ return;
+ }
+
+ // Get the index of the dragged element within its parent
+ startIndex = _index(target, options.draggable);
+
+ // Check filter
+ if (typeof filter === 'function') {
+ if (filter.call(this, evt, target, this)) {
+ _dispatchEvent(_this, originalTarget, 'filter', target, el, startIndex);
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+ else if (filter) {
+ filter = filter.split(',').some(function (criteria) {
+ criteria = _closest(originalTarget, criteria.trim(), el);
+
+ if (criteria) {
+ _dispatchEvent(_this, criteria, 'filter', target, el, startIndex);
+ return true;
+ }
+ });
+
+ if (filter) {
+ evt.preventDefault();
+ return; // cancel dnd
+ }
+ }
+
+ // Prepare `dragstart`
+ this._prepareDragStart(evt, touch, target, startIndex);
+ },
+
+ _prepareDragStart: function (/** Event */evt, /** Touch */touch, /** HTMLElement */target, /** Number */startIndex) {
+ var _this = this,
+ el = _this.el,
+ options = _this.options,
+ ownerDocument = el.ownerDocument,
+ dragStartFn;
+
+ if (target && !dragEl && (target.parentNode === el)) {
+ tapEvt = evt;
+
+ rootEl = el;
+ dragEl = target;
+ parentEl = dragEl.parentNode;
+ nextEl = dragEl.nextSibling;
+ activeGroup = options.group;
+ oldIndex = startIndex;
+
+ this._lastX = (touch || evt).clientX;
+ this._lastY = (touch || evt).clientY;
+
+ dragStartFn = function () {
+ // Delayed drag has been triggered
+ // we can re-enable the events: touchmove/mousemove
+ _this._disableDelayedDrag();
+
+ // Make the element draggable
+ dragEl.draggable = true;
+
+ // Chosen item
+ _toggleClass(dragEl, _this.options.chosenClass, true);
+
+ // Bind the events: dragstart/dragend
+ _this._triggerDragStart(touch);
+
+ // Drag start event
+ _dispatchEvent(_this, rootEl, 'choose', dragEl, rootEl, oldIndex);
+ };
+
+ // Disable "draggable"
+ options.ignore.split(',').forEach(function (criteria) {
+ _find(dragEl, criteria.trim(), _disableDraggable);
+ });
+
+ _on(ownerDocument, 'mouseup', _this._onDrop);
+ _on(ownerDocument, 'touchend', _this._onDrop);
+ _on(ownerDocument, 'touchcancel', _this._onDrop);
+
+ if (options.delay) {
+ // If the user moves the pointer or let go the click or touch
+ // before the delay has been reached:
+ // disable the delayed drag
+ _on(ownerDocument, 'mouseup', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchend', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchcancel', _this._disableDelayedDrag);
+ _on(ownerDocument, 'mousemove', _this._disableDelayedDrag);
+ _on(ownerDocument, 'touchmove', _this._disableDelayedDrag);
+
+ _this._dragStartTimer = setTimeout(dragStartFn, options.delay);
+ } else {
+ dragStartFn();
+ }
+ }
+ },
+
+ _disableDelayedDrag: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ clearTimeout(this._dragStartTimer);
+ _off(ownerDocument, 'mouseup', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchend', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchcancel', this._disableDelayedDrag);
+ _off(ownerDocument, 'mousemove', this._disableDelayedDrag);
+ _off(ownerDocument, 'touchmove', this._disableDelayedDrag);
+ },
+
+ _triggerDragStart: function (/** Touch */touch) {
+ if (touch) {
+ // Touch device support
+ tapEvt = {
+ target: dragEl,
+ clientX: touch.clientX,
+ clientY: touch.clientY
+ };
+
+ this._onDragStart(tapEvt, 'touch');
+ }
+ else if (!this.nativeDraggable) {
+ this._onDragStart(tapEvt, true);
+ }
+ else {
+ _on(dragEl, 'dragend', this);
+ _on(rootEl, 'dragstart', this._onDragStart);
+ }
+
+ try {
+ if (document.selection) {
+ document.selection.empty();
+ } else {
+ window.getSelection().removeAllRanges();
+ }
+ } catch (err) {
+ }
+ },
+
+ _dragStarted: function () {
+ if (rootEl && dragEl) {
+ // Apply effect
+ _toggleClass(dragEl, this.options.ghostClass, true);
+
+ Sortable.active = this;
+
+ // Drag start event
+ _dispatchEvent(this, rootEl, 'start', dragEl, rootEl, oldIndex);
+ }
+ },
+
+ _emulateDragOver: function () {
+ if (touchEvt) {
+ if (this._lastX === touchEvt.clientX && this._lastY === touchEvt.clientY) {
+ return;
+ }
+
+ this._lastX = touchEvt.clientX;
+ this._lastY = touchEvt.clientY;
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', 'none');
+ }
+
+ var target = document.elementFromPoint(touchEvt.clientX, touchEvt.clientY),
+ parent = target,
+ groupName = ' ' + this.options.group.name + '',
+ i = touchDragOverListeners.length;
+
+ if (parent) {
+ do {
+ if (parent[expando] && parent[expando].options.groups.indexOf(groupName) > -1) {
+ while (i--) {
+ touchDragOverListeners[i]({
+ clientX: touchEvt.clientX,
+ clientY: touchEvt.clientY,
+ target: target,
+ rootEl: parent
+ });
+ }
+
+ break;
+ }
+
+ target = parent; // store last element
+ }
+ /* jshint boss:true */
+ while (parent = parent.parentNode);
+ }
+
+ if (!supportCssPointerEvents) {
+ _css(ghostEl, 'display', '');
+ }
+ }
+ },
+
+
+ _onTouchMove: function (/**TouchEvent*/evt) {
+ if (tapEvt) {
+ var options = this.options,
+ fallbackTolerance = options.fallbackTolerance,
+ touch = evt.touches ? evt.touches[0] : evt,
+ dx = touch.clientX - tapEvt.clientX,
+ dy = touch.clientY - tapEvt.clientY,
+ translate3d = evt.touches ? 'translate3d(' + dx + 'px,' + dy + 'px,0)' : 'translate(' + dx + 'px,' + dy + 'px)';
+
+ // only set the status to dragging, when we are actually dragging
+ if (!Sortable.active) {
+ if (fallbackTolerance &&
+ min(abs(touch.clientX - this._lastX), abs(touch.clientY - this._lastY)) < fallbackTolerance
+ ) {
+ return;
+ }
+
+ this._dragStarted();
+ }
+
+ // as well as creating the ghost element on the document body
+ this._appendGhost();
+
+ moved = true;
+ touchEvt = touch;
+
+ _css(ghostEl, 'webkitTransform', translate3d);
+ _css(ghostEl, 'mozTransform', translate3d);
+ _css(ghostEl, 'msTransform', translate3d);
+ _css(ghostEl, 'transform', translate3d);
+
+ evt.preventDefault();
+ }
+ },
+
+ _appendGhost: function () {
+ if (!ghostEl) {
+ var rect = dragEl.getBoundingClientRect(),
+ css = _css(dragEl),
+ options = this.options,
+ ghostRect;
+
+ ghostEl = dragEl.cloneNode(true);
+
+ _toggleClass(ghostEl, options.ghostClass, false);
+ _toggleClass(ghostEl, options.fallbackClass, true);
+
+ _css(ghostEl, 'top', rect.top - parseInt(css.marginTop, 10));
+ _css(ghostEl, 'left', rect.left - parseInt(css.marginLeft, 10));
+ _css(ghostEl, 'width', rect.width);
+ _css(ghostEl, 'height', rect.height);
+ _css(ghostEl, 'opacity', '0.8');
+ _css(ghostEl, 'position', 'fixed');
+ _css(ghostEl, 'zIndex', '100000');
+ _css(ghostEl, 'pointerEvents', 'none');
+
+ options.fallbackOnBody && document.body.appendChild(ghostEl) || rootEl.appendChild(ghostEl);
+
+ // Fixing dimensions.
+ ghostRect = ghostEl.getBoundingClientRect();
+ _css(ghostEl, 'width', rect.width * 2 - ghostRect.width);
+ _css(ghostEl, 'height', rect.height * 2 - ghostRect.height);
+ }
+ },
+
+ _onDragStart: function (/**Event*/evt, /**boolean*/useFallback) {
+ var dataTransfer = evt.dataTransfer,
+ options = this.options;
+
+ this._offUpEvents();
+
+ if (activeGroup.pull == 'clone') {
+ cloneEl = dragEl.cloneNode(true);
+ _css(cloneEl, 'display', 'none');
+ rootEl.insertBefore(cloneEl, dragEl);
+ _dispatchEvent(this, rootEl, 'clone', dragEl);
+ }
+
+ if (useFallback) {
+ if (useFallback === 'touch') {
+ // Bind touch events
+ _on(document, 'touchmove', this._onTouchMove);
+ _on(document, 'touchend', this._onDrop);
+ _on(document, 'touchcancel', this._onDrop);
+ } else {
+ // Old brwoser
+ _on(document, 'mousemove', this._onTouchMove);
+ _on(document, 'mouseup', this._onDrop);
+ }
+
+ this._loopId = setInterval(this._emulateDragOver, 50);
+ }
+ else {
+ if (dataTransfer) {
+ dataTransfer.effectAllowed = 'move';
+ options.setData && options.setData.call(this, dataTransfer, dragEl);
+ }
+
+ _on(document, 'drop', this);
+ setTimeout(this._dragStarted, 0);
+ }
+ },
+
+ _onDragOver: function (/**Event*/evt) {
+ var el = this.el,
+ target,
+ dragRect,
+ revert,
+ options = this.options,
+ group = options.group,
+ groupPut = group.put,
+ isOwner = (activeGroup === group),
+ canSort = options.sort;
+
+ if (evt.preventDefault !== void 0) {
+ evt.preventDefault();
+ !options.dragoverBubble && evt.stopPropagation();
+ }
+
+ moved = true;
+
+ if (activeGroup && !options.disabled &&
+ (isOwner
+ ? canSort || (revert = !rootEl.contains(dragEl)) // Reverting item into the original list
+ : activeGroup.pull && groupPut && (
+ (activeGroup.name === group.name) || // by Name
+ (groupPut.indexOf && ~groupPut.indexOf(activeGroup.name)) // by Array
+ )
+ ) &&
+ (evt.rootEl === void 0 || evt.rootEl === this.el) // touch fallback
+ ) {
+ // Smart auto-scrolling
+ _autoScroll(evt, options, this.el);
+
+ if (_silent) {
+ return;
+ }
+
+ target = _closest(evt.target, options.draggable, el);
+ dragRect = dragEl.getBoundingClientRect();
+
+ if (revert) {
+ _cloneHide(true);
+ parentEl = rootEl; // actualization
+
+ if (cloneEl || nextEl) {
+ rootEl.insertBefore(dragEl, cloneEl || nextEl);
+ }
+ else if (!canSort) {
+ rootEl.appendChild(dragEl);
+ }
+
+ return;
+ }
+
+
+ if ((el.children.length === 0) || (el.children[0] === ghostEl) ||
+ (el === evt.target) && (target = _ghostIsLast(el, evt))
+ ) {
+
+ if (target) {
+ if (target.animated) {
+ return;
+ }
+
+ targetRect = target.getBoundingClientRect();
+ }
+
+ _cloneHide(isOwner);
+
+ if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect) !== false) {
+ if (!dragEl.contains(el)) {
+ el.appendChild(dragEl);
+ parentEl = el; // actualization
+ }
+
+ this._animate(dragRect, dragEl);
+ target && this._animate(targetRect, target);
+ }
+ }
+ else if (target && !target.animated && target !== dragEl && (target.parentNode[expando] !== void 0)) {
+ if (lastEl !== target) {
+ lastEl = target;
+ lastCSS = _css(target);
+ lastParentCSS = _css(target.parentNode);
+ }
+
+
+ var targetRect = target.getBoundingClientRect(),
+ width = targetRect.right - targetRect.left,
+ height = targetRect.bottom - targetRect.top,
+ floating = /left|right|inline/.test(lastCSS.cssFloat + lastCSS.display)
+ || (lastParentCSS.display == 'flex' && lastParentCSS['flex-direction'].indexOf('row') === 0),
+ isWide = (target.offsetWidth > dragEl.offsetWidth),
+ isLong = (target.offsetHeight > dragEl.offsetHeight),
+ halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
+ nextSibling = target.nextElementSibling,
+ moveVector = _onMove(rootEl, el, dragEl, dragRect, target, targetRect),
+ after
+ ;
+
+ if (moveVector !== false) {
+ _silent = true;
+ setTimeout(_unsilent, 30);
+
+ _cloneHide(isOwner);
+
+ if (moveVector === 1 || moveVector === -1) {
+ after = (moveVector === 1);
+ }
+ else if (floating) {
+ var elTop = dragEl.offsetTop,
+ tgTop = target.offsetTop;
+
+ if (elTop === tgTop) {
+ after = (target.previousElementSibling === dragEl) && !isWide || halfway && isWide;
+ }
+ else if (target.previousElementSibling === dragEl || dragEl.previousElementSibling === target) {
+ after = (evt.clientY - targetRect.top) / height > 0.5;
+ } else {
+ after = tgTop > elTop;
+ }
+ } else {
+ after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
+ }
+
+ if (!dragEl.contains(el)) {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
+ } else {
+ target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
+ }
+ }
+
+ parentEl = dragEl.parentNode; // actualization
+
+ this._animate(dragRect, dragEl);
+ this._animate(targetRect, target);
+ }
+ }
+ }
+ },
+
+ _animate: function (prevRect, target) {
+ var ms = this.options.animation;
+
+ if (ms) {
+ var currentRect = target.getBoundingClientRect();
+
+ _css(target, 'transition', 'none');
+ _css(target, 'transform', 'translate3d('
+ + (prevRect.left - currentRect.left) + 'px,'
+ + (prevRect.top - currentRect.top) + 'px,0)'
+ );
+
+ target.offsetWidth; // repaint
+
+ _css(target, 'transition', 'all ' + ms + 'ms');
+ _css(target, 'transform', 'translate3d(0,0,0)');
+
+ clearTimeout(target.animated);
+ target.animated = setTimeout(function () {
+ _css(target, 'transition', '');
+ _css(target, 'transform', '');
+ target.animated = false;
+ }, ms);
+ }
+ },
+
+ _offUpEvents: function () {
+ var ownerDocument = this.el.ownerDocument;
+
+ _off(document, 'touchmove', this._onTouchMove);
+ _off(ownerDocument, 'mouseup', this._onDrop);
+ _off(ownerDocument, 'touchend', this._onDrop);
+ _off(ownerDocument, 'touchcancel', this._onDrop);
+ },
+
+ _onDrop: function (/**Event*/evt) {
+ var el = this.el,
+ options = this.options;
+
+ clearInterval(this._loopId);
+ clearInterval(autoScroll.pid);
+ clearTimeout(this._dragStartTimer);
+
+ // Unbind events
+ _off(document, 'mousemove', this._onTouchMove);
+
+ if (this.nativeDraggable) {
+ _off(document, 'drop', this);
+ _off(el, 'dragstart', this._onDragStart);
+ }
+
+ this._offUpEvents();
+
+ if (evt) {
+ if (moved) {
+ evt.preventDefault();
+ !options.dropBubble && evt.stopPropagation();
+ }
+
+ ghostEl && ghostEl.parentNode.removeChild(ghostEl);
+
+ if (dragEl) {
+ if (this.nativeDraggable) {
+ _off(dragEl, 'dragend', this);
+ }
+
+ _disableDraggable(dragEl);
+
+ // Remove class's
+ _toggleClass(dragEl, this.options.ghostClass, false);
+ _toggleClass(dragEl, this.options.chosenClass, false);
+
+ if (rootEl !== parentEl) {
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // drag from one list and drop into another
+ _dispatchEvent(null, parentEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+
+ // Add event
+ _dispatchEvent(null, parentEl, 'add', dragEl, rootEl, oldIndex, newIndex);
+
+ // Remove event
+ _dispatchEvent(this, rootEl, 'remove', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ else {
+ // Remove clone
+ cloneEl && cloneEl.parentNode.removeChild(cloneEl);
+
+ if (dragEl.nextSibling !== nextEl) {
+ // Get the index of the dragged element within its parent
+ newIndex = _index(dragEl, options.draggable);
+
+ if (newIndex >= 0) {
+ // drag & drop within the same list
+ _dispatchEvent(this, rootEl, 'update', dragEl, rootEl, oldIndex, newIndex);
+ _dispatchEvent(this, rootEl, 'sort', dragEl, rootEl, oldIndex, newIndex);
+ }
+ }
+ }
+
+ if (Sortable.active) {
+ if (newIndex === null || newIndex === -1) {
+ newIndex = oldIndex;
+ }
+
+ _dispatchEvent(this, rootEl, 'end', dragEl, rootEl, oldIndex, newIndex);
+
+ // Save sorting
+ this.save();
+ }
+ }
+
+ }
+
+ this._nulling();
+ },
+
+ _nulling: function () {
+ rootEl =
+ dragEl =
+ parentEl =
+ ghostEl =
+ nextEl =
+ cloneEl =
+
+ scrollEl =
+ scrollParentEl =
+
+ tapEvt =
+ touchEvt =
+
+ moved =
+ newIndex =
+
+ lastEl =
+ lastCSS =
+
+ activeGroup =
+ Sortable.active = null;
+ },
+
+ handleEvent: function (/**Event*/evt) {
+ var type = evt.type;
+
+ if (type === 'dragover' || type === 'dragenter') {
+ if (dragEl) {
+ this._onDragOver(evt);
+ _globalDragOver(evt);
+ }
+ }
+ else if (type === 'drop' || type === 'dragend') {
+ this._onDrop(evt);
+ }
+ },
+
+
+ /**
+ * Serializes the item into an array of string.
+ * @returns {String[]}
+ */
+ toArray: function () {
+ var order = [],
+ el,
+ children = this.el.children,
+ i = 0,
+ n = children.length,
+ options = this.options;
+
+ for (; i < n; i++) {
+ el = children[i];
+ if (_closest(el, options.draggable, this.el)) {
+ order.push(el.getAttribute(options.dataIdAttr) || _generateId(el));
+ }
+ }
+
+ return order;
+ },
+
+
+ /**
+ * Sorts the elements according to the array.
+ * @param {String[]} order order of the items
+ */
+ sort: function (order) {
+ var items = {}, rootEl = this.el;
+
+ this.toArray().forEach(function (id, i) {
+ var el = rootEl.children[i];
+
+ if (_closest(el, this.options.draggable, rootEl)) {
+ items[id] = el;
+ }
+ }, this);
+
+ order.forEach(function (id) {
+ if (items[id]) {
+ rootEl.removeChild(items[id]);
+ rootEl.appendChild(items[id]);
+ }
+ });
+ },
+
+
+ /**
+ * Save the current sorting
+ */
+ save: function () {
+ var store = this.options.store;
+ store && store.set(this);
+ },
+
+
+ /**
+ * For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.
+ * @param {HTMLElement} el
+ * @param {String} [selector] default: `options.draggable`
+ * @returns {HTMLElement|null}
+ */
+ closest: function (el, selector) {
+ return _closest(el, selector || this.options.draggable, this.el);
+ },
+
+
+ /**
+ * Set/get option
+ * @param {string} name
+ * @param {*} [value]
+ * @returns {*}
+ */
+ option: function (name, value) {
+ var options = this.options;
+
+ if (value === void 0) {
+ return options[name];
+ } else {
+ options[name] = value;
+
+ if (name === 'group') {
+ _prepareGroup(options);
+ }
+ }
+ },
+
+
+ /**
+ * Destroy
+ */
+ destroy: function () {
+ var el = this.el;
+
+ el[expando] = null;
+
+ _off(el, 'mousedown', this._onTapStart);
+ _off(el, 'touchstart', this._onTapStart);
+
+ if (this.nativeDraggable) {
+ _off(el, 'dragover', this);
+ _off(el, 'dragenter', this);
+ }
+
+ // Remove draggable attributes
+ Array.prototype.forEach.call(el.querySelectorAll('[draggable]'), function (el) {
+ el.removeAttribute('draggable');
+ });
+
+ touchDragOverListeners.splice(touchDragOverListeners.indexOf(this._onDragOver), 1);
+
+ this._onDrop();
+
+ this.el = el = null;
+ }
+ };
+
+
+ function _cloneHide(state) {
+ if (cloneEl && (cloneEl.state !== state)) {
+ _css(cloneEl, 'display', state ? 'none' : '');
+ !state && cloneEl.state && rootEl.insertBefore(cloneEl, dragEl);
+ cloneEl.state = state;
+ }
+ }
+
+
+ function _closest(/**HTMLElement*/el, /**String*/selector, /**HTMLElement*/ctx) {
+ if (el) {
+ ctx = ctx || document;
+
+ do {
+ if ((selector === '>*' && el.parentNode === ctx) || _matches(el, selector)) {
+ return el;
+ }
+ }
+ while (el !== ctx && (el = el.parentNode));
+ }
+
+ return null;
+ }
+
+
+ function _globalDragOver(/**Event*/evt) {
+ if (evt.dataTransfer) {
+ evt.dataTransfer.dropEffect = 'move';
+ }
+ evt.preventDefault();
+ }
+
+
+ function _on(el, event, fn) {
+ el.addEventListener(event, fn, false);
+ }
+
+
+ function _off(el, event, fn) {
+ el.removeEventListener(event, fn, false);
+ }
+
+
+ function _toggleClass(el, name, state) {
+ if (el) {
+ if (el.classList) {
+ el.classList[state ? 'add' : 'remove'](name);
+ }
+ else {
+ var className = (' ' + el.className + ' ').replace(RSPACE, ' ').replace(' ' + name + ' ', ' ');
+ el.className = (className + (state ? ' ' + name : '')).replace(RSPACE, ' ');
+ }
+ }
+ }
+
+
+ function _css(el, prop, val) {
+ var style = el && el.style;
+
+ if (style) {
+ if (val === void 0) {
+ if (document.defaultView && document.defaultView.getComputedStyle) {
+ val = document.defaultView.getComputedStyle(el, '');
+ }
+ else if (el.currentStyle) {
+ val = el.currentStyle;
+ }
+
+ return prop === void 0 ? val : val[prop];
+ }
+ else {
+ if (!(prop in style)) {
+ prop = '-webkit-' + prop;
+ }
+
+ style[prop] = val + (typeof val === 'string' ? '' : 'px');
+ }
+ }
+ }
+
+
+ function _find(ctx, tagName, iterator) {
+ if (ctx) {
+ var list = ctx.getElementsByTagName(tagName), i = 0, n = list.length;
+
+ if (iterator) {
+ for (; i < n; i++) {
+ iterator(list[i], i);
+ }
+ }
+
+ return list;
+ }
+
+ return [];
+ }
+
+
+
+ function _dispatchEvent(sortable, rootEl, name, targetEl, fromEl, startIndex, newIndex) {
+ var evt = document.createEvent('Event'),
+ options = (sortable || rootEl[expando]).options,
+ onName = 'on' + name.charAt(0).toUpperCase() + name.substr(1);
+
+ evt.initEvent(name, true, true);
+
+ evt.to = rootEl;
+ evt.from = fromEl || rootEl;
+ evt.item = targetEl || rootEl;
+ evt.clone = cloneEl;
+
+ evt.oldIndex = startIndex;
+ evt.newIndex = newIndex;
+
+ rootEl.dispatchEvent(evt);
+
+ if (options[onName]) {
+ options[onName].call(sortable, evt);
+ }
+ }
+
+
+ function _onMove(fromEl, toEl, dragEl, dragRect, targetEl, targetRect) {
+ var evt,
+ sortable = fromEl[expando],
+ onMoveFn = sortable.options.onMove,
+ retVal;
+
+ evt = document.createEvent('Event');
+ evt.initEvent('move', true, true);
+
+ evt.to = toEl;
+ evt.from = fromEl;
+ evt.dragged = dragEl;
+ evt.draggedRect = dragRect;
+ evt.related = targetEl || toEl;
+ evt.relatedRect = targetRect || toEl.getBoundingClientRect();
+
+ fromEl.dispatchEvent(evt);
+
+ if (onMoveFn) {
+ retVal = onMoveFn.call(sortable, evt);
+ }
+
+ return retVal;
+ }
+
+
+ function _disableDraggable(el) {
+ el.draggable = false;
+ }
+
+
+ function _unsilent() {
+ _silent = false;
+ }
+
+
+ /** @returns {HTMLElement|false} */
+ function _ghostIsLast(el, evt) {
+ var lastEl = el.lastElementChild,
+ rect = lastEl.getBoundingClientRect();
+
+ return ((evt.clientY - (rect.top + rect.height) > 5) || (evt.clientX - (rect.right + rect.width) > 5)) && lastEl; // min delta
+ }
+
+
+ /**
+ * Generate id
+ * @param {HTMLElement} el
+ * @returns {String}
+ * @private
+ */
+ function _generateId(el) {
+ var str = el.tagName + el.className + el.src + el.href + el.textContent,
+ i = str.length,
+ sum = 0;
+
+ while (i--) {
+ sum += str.charCodeAt(i);
+ }
+
+ return sum.toString(36);
+ }
+
+ /**
+ * Returns the index of an element within its parent for a selected set of
+ * elements
+ * @param {HTMLElement} el
+ * @param {selector} selector
+ * @return {number}
+ */
+ function _index(el, selector) {
+ var index = 0;
+
+ if (!el || !el.parentNode) {
+ return -1;
+ }
+
+ while (el && (el = el.previousElementSibling)) {
+ if ((el.nodeName.toUpperCase() !== 'TEMPLATE') && (selector === '>*' || _matches(el, selector))) {
+ index++;
+ }
+ }
+
+ return index;
+ }
+
+ function _matches(/**HTMLElement*/el, /**String*/selector) {
+ if (el) {
+ selector = selector.split('.');
+
+ var tag = selector.shift().toUpperCase(),
+ re = new RegExp('\\s(' + selector.join('|') + ')(?=\\s)', 'g');
+
+ return (
+ (tag === '' || el.nodeName.toUpperCase() == tag) &&
+ (!selector.length || ((' ' + el.className + ' ').match(re) || []).length == selector.length)
+ );
+ }
+
+ return false;
+ }
+
+ function _throttle(callback, ms) {
+ var args, _this;
+
+ return function () {
+ if (args === void 0) {
+ args = arguments;
+ _this = this;
+
+ setTimeout(function () {
+ if (args.length === 1) {
+ callback.call(_this, args[0]);
+ } else {
+ callback.apply(_this, args);
+ }
+
+ args = void 0;
+ }, ms);
+ }
+ };
+ }
+
+ function _extend(dst, src) {
+ if (dst && src) {
+ for (var key in src) {
+ if (src.hasOwnProperty(key)) {
+ dst[key] = src[key];
+ }
+ }
+ }
+
+ return dst;
+ }
+
+
+ // Export utils
+ Sortable.utils = {
+ on: _on,
+ off: _off,
+ css: _css,
+ find: _find,
+ is: function (el, selector) {
+ return !!_closest(el, selector, el);
+ },
+ extend: _extend,
+ throttle: _throttle,
+ closest: _closest,
+ toggleClass: _toggleClass,
+ index: _index
+ };
+
+
+ /**
+ * Create sortable instance
+ * @param {HTMLElement} el
+ * @param {Object} [options]
+ */
+ Sortable.create = function (el, options) {
+ return new Sortable(el, options);
+ };
+
+
+ // Export
+ Sortable.version = '1.4.2';
+ return Sortable;
+});
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
index cfa49e72c50..cfa49e72c50 100755..100644
--- a/vendor/assets/javascripts/autosize.js
+++ b/vendor/assets/javascripts/autosize.js
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
index 1b1f4f0bd63..39d7d2306f8 100644
--- a/vendor/assets/javascripts/clipboard.js
+++ b/vendor/assets/javascripts/clipboard.js
@@ -154,12 +154,12 @@ function E () {
E.prototype = {
on: function (name, callback, ctx) {
var e = this.e || (this.e = {});
-
+
(e[name] || (e[name] = [])).push({
fn: callback,
ctx: ctx
});
-
+
return this;
},
@@ -169,7 +169,7 @@ E.prototype = {
self.off(name, fn);
callback.apply(ctx, arguments);
};
-
+
return this.on(name, fn, ctx);
},
@@ -178,11 +178,11 @@ E.prototype = {
var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
var i = 0;
var len = evtArr.length;
-
+
for (i; i < len; i++) {
evtArr[i].fn.apply(evtArr[i].ctx, data);
}
-
+
return this;
},
@@ -190,21 +190,21 @@ E.prototype = {
var e = this.e || (this.e = {});
var evts = e[name];
var liveEvents = [];
-
+
if (evts && callback) {
for (var i = 0, len = evts.length; i < len; i++) {
if (evts[i].fn !== callback) liveEvents.push(evts[i]);
}
}
-
+
// Remove event from queue to prevent memory leak
// Suggested by https://github.com/lazd
// Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
- (liveEvents.length)
+ (liveEvents.length)
? e[name] = liveEvents
: delete e[name];
-
+
return this;
}
};
@@ -618,4 +618,4 @@ exports['default'] = Clipboard;
module.exports = exports['default'];
},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
-}); \ No newline at end of file
+});
diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
index 7ba17766b70..7ba17766b70 100755..100644
--- a/vendor/assets/javascripts/jquery.scrollTo.js
+++ b/vendor/assets/javascripts/jquery.scrollTo.js
diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js
new file mode 100644
index 00000000000..bc451506b6a
--- /dev/null
+++ b/vendor/assets/javascripts/task_list.js
@@ -0,0 +1,119 @@
+
+/*= provides tasklist:enabled */
+
+
+/*= provides tasklist:disabled */
+
+
+/*= provides tasklist:change */
+
+
+/*= provides tasklist:changed */
+
+(function() {
+ var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem,
+ 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; };
+
+ incomplete = "[ ]";
+
+ complete = "[x]";
+
+ escapePattern = function(str) {
+ return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]");
+ };
+
+ incompletePattern = RegExp("" + (escapePattern(incomplete)));
+
+ completePattern = RegExp("" + (escapePattern(complete)));
+
+ itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))");
+
+ codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg;
+
+ itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g");
+
+ updateTaskListItem = function(source, itemIndex, checked) {
+ var clean, index, line, result;
+ clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n");
+ index = 0;
+ result = (function() {
+ var i, len, ref, results;
+ ref = source.split("\n");
+ results = [];
+ for (i = 0, len = ref.length; i < len; i++) {
+ line = ref[i];
+ if (indexOf.call(clean, line) >= 0 && line.match(itemPattern)) {
+ index += 1;
+ if (index === itemIndex) {
+ line = checked ? line.replace(incompletePattern, complete) : line.replace(completePattern, incomplete);
+ }
+ }
+ results.push(line);
+ }
+ return results;
+ })();
+ return result.join("\n");
+ };
+
+ updateTaskList = function($item) {
+ var $container, $field, checked, event, index;
+ $container = $item.closest('.js-task-list-container');
+ $field = $container.find('.js-task-list-field');
+ index = 1 + $container.find('.task-list-item-checkbox').index($item);
+ checked = $item.prop('checked');
+ event = $.Event('tasklist:change');
+ $field.trigger(event, [index, checked]);
+ if (!event.isDefaultPrevented()) {
+ $field.val(updateTaskListItem($field.val(), index, checked));
+ $field.trigger('change');
+ return $field.trigger('tasklist:changed', [index, checked]);
+ }
+ };
+
+ $(document).on('change', '.task-list-item-checkbox', function() {
+ return updateTaskList($(this));
+ });
+
+ enableTaskList = function($container) {
+ if ($container.find('.js-task-list-field').length > 0) {
+ $container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null);
+ return $container.addClass('is-task-list-enabled').trigger('tasklist:enabled');
+ }
+ };
+
+ enableTaskLists = function($containers) {
+ var container, i, len, results;
+ results = [];
+ for (i = 0, len = $containers.length; i < len; i++) {
+ container = $containers[i];
+ results.push(enableTaskList($(container)));
+ }
+ return results;
+ };
+
+ disableTaskList = function($container) {
+ $container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled');
+ return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled');
+ };
+
+ disableTaskLists = function($containers) {
+ var container, i, len, results;
+ results = [];
+ for (i = 0, len = $containers.length; i < len; i++) {
+ container = $containers[i];
+ results.push(disableTaskList($(container)));
+ }
+ return results;
+ };
+
+ $.fn.taskList = function(method) {
+ var $container, methods;
+ $container = $(this).closest('.js-task-list-container');
+ methods = {
+ enable: enableTaskLists,
+ disable: disableTaskLists
+ };
+ return methods[method || 'enable']($container);
+ };
+
+}).call(this);
diff --git a/vendor/assets/javascripts/task_list.js.coffee b/vendor/assets/javascripts/task_list.js.coffee
deleted file mode 100644
index 584751af8ea..00000000000
--- a/vendor/assets/javascripts/task_list.js.coffee
+++ /dev/null
@@ -1,258 +0,0 @@
-# The MIT License (MIT)
-#
-# Copyright (c) 2014 GitHub, Inc.
-#
-# 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.
-
-# TaskList Behavior
-#
-#= provides tasklist:enabled
-#= provides tasklist:disabled
-#= provides tasklist:change
-#= provides tasklist:changed
-#
-#
-# Enables Task List update behavior.
-#
-# ### Example Markup
-#
-# <div class="js-task-list-container">
-# <ul class="task-list">
-# <li class="task-list-item">
-# <input type="checkbox" class="js-task-list-item-checkbox" disabled />
-# text
-# </li>
-# </ul>
-# <form>
-# <textarea class="js-task-list-field">- [ ] text</textarea>
-# </form>
-# </div>
-#
-# ### Specification
-#
-# TaskLists MUST be contained in a `(div).js-task-list-container`.
-#
-# TaskList Items SHOULD be an a list (`UL`/`OL`) element.
-#
-# Task list items MUST match `(input).task-list-item-checkbox` and MUST be
-# `disabled` by default.
-#
-# TaskLists MUST have a `(textarea).js-task-list-field` form element whose
-# `value` attribute is the source (Markdown) to be udpated. The source MUST
-# follow the syntax guidelines.
-#
-# TaskList updates trigger `tasklist:change` events. If the change is
-# successful, `tasklist:changed` is fired. The change can be canceled.
-#
-# jQuery is required.
-#
-# ### Methods
-#
-# `.taskList('enable')` or `.taskList()`
-#
-# Enables TaskList updates for the container.
-#
-# `.taskList('disable')`
-#
-# Disables TaskList updates for the container.
-#
-## ### Events
-#
-# `tasklist:enabled`
-#
-# Fired when the TaskList is enabled.
-#
-# * **Synchronicity** Sync
-# * **Bubbles** Yes
-# * **Cancelable** No
-# * **Target** `.js-task-list-container`
-#
-# `tasklist:disabled`
-#
-# Fired when the TaskList is disabled.
-#
-# * **Synchronicity** Sync
-# * **Bubbles** Yes
-# * **Cancelable** No
-# * **Target** `.js-task-list-container`
-#
-# `tasklist:change`
-#
-# Fired before the TaskList item change takes affect.
-#
-# * **Synchronicity** Sync
-# * **Bubbles** Yes
-# * **Cancelable** Yes
-# * **Target** `.js-task-list-field`
-#
-# `tasklist:changed`
-#
-# Fired once the TaskList item change has taken affect.
-#
-# * **Synchronicity** Sync
-# * **Bubbles** Yes
-# * **Cancelable** No
-# * **Target** `.js-task-list-field`
-#
-# ### NOTE
-#
-# Task list checkboxes are rendered as disabled by default because rendered
-# user content is cached without regard for the viewer.
-
-incomplete = "[ ]"
-complete = "[x]"
-
-# Escapes the String for regular expression matching.
-escapePattern = (str) ->
- str.
- replace(/([\[\]])/g, "\\$1"). # escape square brackets
- replace(/\s/, "\\s"). # match all white space
- replace("x", "[xX]") # match all cases
-
-incompletePattern = ///
- #{escapePattern(incomplete)}
-///
-completePattern = ///
- #{escapePattern(complete)}
-///
-
-# Pattern used to identify all task list items.
-# Useful when you need iterate over all items.
-itemPattern = ///
- ^
- (?: # prefix, consisting of
- \s* # optional leading whitespace
- (?:>\s*)* # zero or more blockquotes
- (?:[-+*]|(?:\d+\.)) # list item indicator
- )
- \s* # optional whitespace prefix
- ( # checkbox
- #{escapePattern(complete)}|
- #{escapePattern(incomplete)}
- )
- \s+ # is followed by whitespace
- (?!
- \(.*?\) # is not part of a [foo](url) link
- )
- (?= # and is followed by zero or more links
- (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)*
- (?:[^\[]|$) # and either a non-link or the end of the string
- )
-///
-
-# Used to filter out code fences from the source for comparison only.
-# http://rubular.com/r/x5EwZVrloI
-# Modified slightly due to issues with JS
-codeFencesPattern = ///
- ^`{3} # ```
- (?:\s*\w+)? # followed by optional language
- [\S\s] # whitespace
- .* # code
- [\S\s] # whitespace
- ^`{3}$ # ```
-///mg
-
-# Used to filter out potential mismatches (items not in lists).
-# http://rubular.com/r/OInl6CiePy
-itemsInParasPattern = ///
- ^
- (
- #{escapePattern(complete)}|
- #{escapePattern(incomplete)}
- )
- .+
- $
-///g
-
-# Given the source text, updates the appropriate task list item to match the
-# given checked value.
-#
-# Returns the updated String text.
-updateTaskListItem = (source, itemIndex, checked) ->
- clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').
- replace(itemsInParasPattern, '').split("\n")
- index = 0
- result = for line in source.split("\n")
- if line in clean && line.match(itemPattern)
- index += 1
- if index == itemIndex
- line =
- if checked
- line.replace(incompletePattern, complete)
- else
- line.replace(completePattern, incomplete)
- line
- result.join("\n")
-
-# Updates the $field value to reflect the state of $item.
-# Triggers the `tasklist:change` event before the value has changed, and fires
-# a `tasklist:changed` event once the value has changed.
-updateTaskList = ($item) ->
- $container = $item.closest '.js-task-list-container'
- $field = $container.find '.js-task-list-field'
- index = 1 + $container.find('.task-list-item-checkbox').index($item)
- checked = $item.prop 'checked'
-
- event = $.Event 'tasklist:change'
- $field.trigger event, [index, checked]
-
- unless event.isDefaultPrevented()
- $field.val updateTaskListItem($field.val(), index, checked)
- $field.trigger 'change'
- $field.trigger 'tasklist:changed', [index, checked]
-
-# When the task list item checkbox is updated, submit the change
-$(document).on 'change', '.task-list-item-checkbox', ->
- updateTaskList $(this)
-
-# Enables TaskList item changes.
-enableTaskList = ($container) ->
- if $container.find('.js-task-list-field').length > 0
- $container.
- find('.task-list-item').addClass('enabled').
- find('.task-list-item-checkbox').attr('disabled', null)
- $container.addClass('is-task-list-enabled').
- trigger 'tasklist:enabled'
-
-# Enables a collection of TaskList containers.
-enableTaskLists = ($containers) ->
- for container in $containers
- enableTaskList $(container)
-
-# Disable TaskList item changes.
-disableTaskList = ($container) ->
- $container.
- find('.task-list-item').removeClass('enabled').
- find('.task-list-item-checkbox').attr('disabled', 'disabled')
- $container.removeClass('is-task-list-enabled').
- trigger 'tasklist:disabled'
-
-# Disables a collection of TaskList containers.
-disableTaskLists = ($containers) ->
- for container in $containers
- disableTaskList $(container)
-
-$.fn.taskList = (method) ->
- $container = $(this).closest('.js-task-list-container')
-
- methods =
- enable: enableTaskLists
- disable: disableTaskLists
-
- methods[method || 'enable']($container)
diff --git a/vendor/assets/javascripts/vue-resource.full.js b/vendor/assets/javascripts/vue-resource.full.js
new file mode 100644
index 00000000000..d7981dbec7e
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.full.js
@@ -0,0 +1,1318 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.VueResource = factory());
+}(this, function () { 'use strict';
+
+ /**
+ * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis)
+ */
+
+ var RESOLVED = 0;
+ var REJECTED = 1;
+ var PENDING = 2;
+
+ function Promise$2(executor) {
+
+ this.state = PENDING;
+ this.value = undefined;
+ this.deferred = [];
+
+ var promise = this;
+
+ try {
+ executor(function (x) {
+ promise.resolve(x);
+ }, function (r) {
+ promise.reject(r);
+ });
+ } catch (e) {
+ promise.reject(e);
+ }
+ }
+
+ Promise$2.reject = function (r) {
+ return new Promise$2(function (resolve, reject) {
+ reject(r);
+ });
+ };
+
+ Promise$2.resolve = function (x) {
+ return new Promise$2(function (resolve, reject) {
+ resolve(x);
+ });
+ };
+
+ Promise$2.all = function all(iterable) {
+ return new Promise$2(function (resolve, reject) {
+ var count = 0,
+ result = [];
+
+ if (iterable.length === 0) {
+ resolve(result);
+ }
+
+ function resolver(i) {
+ return function (x) {
+ result[i] = x;
+ count += 1;
+
+ if (count === iterable.length) {
+ resolve(result);
+ }
+ };
+ }
+
+ for (var i = 0; i < iterable.length; i += 1) {
+ Promise$2.resolve(iterable[i]).then(resolver(i), reject);
+ }
+ });
+ };
+
+ Promise$2.race = function race(iterable) {
+ return new Promise$2(function (resolve, reject) {
+ for (var i = 0; i < iterable.length; i += 1) {
+ Promise$2.resolve(iterable[i]).then(resolve, reject);
+ }
+ });
+ };
+
+ var p$1 = Promise$2.prototype;
+
+ p$1.resolve = function resolve(x) {
+ var promise = this;
+
+ if (promise.state === PENDING) {
+ if (x === promise) {
+ throw new TypeError('Promise settled with itself.');
+ }
+
+ var called = false;
+
+ try {
+ var then = x && x['then'];
+
+ if (x !== null && typeof x === 'object' && typeof then === 'function') {
+ then.call(x, function (x) {
+ if (!called) {
+ promise.resolve(x);
+ }
+ called = true;
+ }, function (r) {
+ if (!called) {
+ promise.reject(r);
+ }
+ called = true;
+ });
+ return;
+ }
+ } catch (e) {
+ if (!called) {
+ promise.reject(e);
+ }
+ return;
+ }
+
+ promise.state = RESOLVED;
+ promise.value = x;
+ promise.notify();
+ }
+ };
+
+ p$1.reject = function reject(reason) {
+ var promise = this;
+
+ if (promise.state === PENDING) {
+ if (reason === promise) {
+ throw new TypeError('Promise settled with itself.');
+ }
+
+ promise.state = REJECTED;
+ promise.value = reason;
+ promise.notify();
+ }
+ };
+
+ p$1.notify = function notify() {
+ var promise = this;
+
+ nextTick(function () {
+ if (promise.state !== PENDING) {
+ while (promise.deferred.length) {
+ var deferred = promise.deferred.shift(),
+ onResolved = deferred[0],
+ onRejected = deferred[1],
+ resolve = deferred[2],
+ reject = deferred[3];
+
+ try {
+ if (promise.state === RESOLVED) {
+ if (typeof onResolved === 'function') {
+ resolve(onResolved.call(undefined, promise.value));
+ } else {
+ resolve(promise.value);
+ }
+ } else if (promise.state === REJECTED) {
+ if (typeof onRejected === 'function') {
+ resolve(onRejected.call(undefined, promise.value));
+ } else {
+ reject(promise.value);
+ }
+ }
+ } catch (e) {
+ reject(e);
+ }
+ }
+ }
+ });
+ };
+
+ p$1.then = function then(onResolved, onRejected) {
+ var promise = this;
+
+ return new Promise$2(function (resolve, reject) {
+ promise.deferred.push([onResolved, onRejected, resolve, reject]);
+ promise.notify();
+ });
+ };
+
+ p$1.catch = function (onRejected) {
+ return this.then(undefined, onRejected);
+ };
+
+ var PromiseObj = window.Promise || Promise$2;
+
+ function Promise$1(executor, context) {
+
+ if (executor instanceof PromiseObj) {
+ this.promise = executor;
+ } else {
+ this.promise = new PromiseObj(executor.bind(context));
+ }
+
+ this.context = context;
+ }
+
+ Promise$1.all = function (iterable, context) {
+ return new Promise$1(PromiseObj.all(iterable), context);
+ };
+
+ Promise$1.resolve = function (value, context) {
+ return new Promise$1(PromiseObj.resolve(value), context);
+ };
+
+ Promise$1.reject = function (reason, context) {
+ return new Promise$1(PromiseObj.reject(reason), context);
+ };
+
+ Promise$1.race = function (iterable, context) {
+ return new Promise$1(PromiseObj.race(iterable), context);
+ };
+
+ var p = Promise$1.prototype;
+
+ p.bind = function (context) {
+ this.context = context;
+ return this;
+ };
+
+ p.then = function (fulfilled, rejected) {
+
+ if (fulfilled && fulfilled.bind && this.context) {
+ fulfilled = fulfilled.bind(this.context);
+ }
+
+ if (rejected && rejected.bind && this.context) {
+ rejected = rejected.bind(this.context);
+ }
+
+ return new Promise$1(this.promise.then(fulfilled, rejected), this.context);
+ };
+
+ p.catch = function (rejected) {
+
+ if (rejected && rejected.bind && this.context) {
+ rejected = rejected.bind(this.context);
+ }
+
+ return new Promise$1(this.promise.catch(rejected), this.context);
+ };
+
+ p.finally = function (callback) {
+
+ return this.then(function (value) {
+ callback.call(this);
+ return value;
+ }, function (reason) {
+ callback.call(this);
+ return PromiseObj.reject(reason);
+ });
+ };
+
+ var debug = false;
+ var util = {};
+ var array = [];
+ function Util (Vue) {
+ util = Vue.util;
+ debug = Vue.config.debug || !Vue.config.silent;
+ }
+
+ function warn(msg) {
+ if (typeof console !== 'undefined' && debug) {
+ console.warn('[VueResource warn]: ' + msg);
+ }
+ }
+
+ function error(msg) {
+ if (typeof console !== 'undefined') {
+ console.error(msg);
+ }
+ }
+
+ function nextTick(cb, ctx) {
+ return util.nextTick(cb, ctx);
+ }
+
+ function trim(str) {
+ return str.replace(/^\s*|\s*$/g, '');
+ }
+
+ var isArray = Array.isArray;
+
+ function isString(val) {
+ return typeof val === 'string';
+ }
+
+ function isBoolean(val) {
+ return val === true || val === false;
+ }
+
+ function isFunction(val) {
+ return typeof val === 'function';
+ }
+
+ function isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+ function isPlainObject(obj) {
+ return isObject(obj) && Object.getPrototypeOf(obj) == Object.prototype;
+ }
+
+ function isFormData(obj) {
+ return typeof FormData !== 'undefined' && obj instanceof FormData;
+ }
+
+ function when(value, fulfilled, rejected) {
+
+ var promise = Promise$1.resolve(value);
+
+ if (arguments.length < 2) {
+ return promise;
+ }
+
+ return promise.then(fulfilled, rejected);
+ }
+
+ function options(fn, obj, opts) {
+
+ opts = opts || {};
+
+ if (isFunction(opts)) {
+ opts = opts.call(obj);
+ }
+
+ return merge(fn.bind({ $vm: obj, $options: opts }), fn, { $options: opts });
+ }
+
+ function each(obj, iterator) {
+
+ var i, key;
+
+ if (typeof obj.length == 'number') {
+ for (i = 0; i < obj.length; i++) {
+ iterator.call(obj[i], obj[i], i);
+ }
+ } else if (isObject(obj)) {
+ for (key in obj) {
+ if (obj.hasOwnProperty(key)) {
+ iterator.call(obj[key], obj[key], key);
+ }
+ }
+ }
+
+ return obj;
+ }
+
+ var assign = Object.assign || _assign;
+
+ function merge(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+ _merge(target, source, true);
+ });
+
+ return target;
+ }
+
+ function defaults(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+
+ for (var key in source) {
+ if (target[key] === undefined) {
+ target[key] = source[key];
+ }
+ }
+ });
+
+ return target;
+ }
+
+ function _assign(target) {
+
+ var args = array.slice.call(arguments, 1);
+
+ args.forEach(function (source) {
+ _merge(target, source);
+ });
+
+ return target;
+ }
+
+ function _merge(target, source, deep) {
+ for (var key in source) {
+ if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
+ if (isPlainObject(source[key]) && !isPlainObject(target[key])) {
+ target[key] = {};
+ }
+ if (isArray(source[key]) && !isArray(target[key])) {
+ target[key] = [];
+ }
+ _merge(target[key], source[key], deep);
+ } else if (source[key] !== undefined) {
+ target[key] = source[key];
+ }
+ }
+ }
+
+ function root (options, next) {
+
+ var url = next(options);
+
+ if (isString(options.root) && !url.match(/^(https?:)?\//)) {
+ url = options.root + '/' + url;
+ }
+
+ return url;
+ }
+
+ function query (options, next) {
+
+ var urlParams = Object.keys(Url.options.params),
+ query = {},
+ url = next(options);
+
+ each(options.params, function (value, key) {
+ if (urlParams.indexOf(key) === -1) {
+ query[key] = value;
+ }
+ });
+
+ query = Url.params(query);
+
+ if (query) {
+ url += (url.indexOf('?') == -1 ? '?' : '&') + query;
+ }
+
+ return url;
+ }
+
+ /**
+ * URL Template v2.0.6 (https://github.com/bramstein/url-template)
+ */
+
+ function expand(url, params, variables) {
+
+ var tmpl = parse(url),
+ expanded = tmpl.expand(params);
+
+ if (variables) {
+ variables.push.apply(variables, tmpl.vars);
+ }
+
+ return expanded;
+ }
+
+ function parse(template) {
+
+ var operators = ['+', '#', '.', '/', ';', '?', '&'],
+ variables = [];
+
+ return {
+ vars: variables,
+ expand: function (context) {
+ return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) {
+ if (expression) {
+
+ var operator = null,
+ values = [];
+
+ if (operators.indexOf(expression.charAt(0)) !== -1) {
+ operator = expression.charAt(0);
+ expression = expression.substr(1);
+ }
+
+ expression.split(/,/g).forEach(function (variable) {
+ var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable);
+ values.push.apply(values, getValues(context, operator, tmp[1], tmp[2] || tmp[3]));
+ variables.push(tmp[1]);
+ });
+
+ if (operator && operator !== '+') {
+
+ var separator = ',';
+
+ if (operator === '?') {
+ separator = '&';
+ } else if (operator !== '#') {
+ separator = operator;
+ }
+
+ return (values.length !== 0 ? operator : '') + values.join(separator);
+ } else {
+ return values.join(',');
+ }
+ } else {
+ return encodeReserved(literal);
+ }
+ });
+ }
+ };
+ }
+
+ function getValues(context, operator, key, modifier) {
+
+ var value = context[key],
+ result = [];
+
+ if (isDefined(value) && value !== '') {
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
+ value = value.toString();
+
+ if (modifier && modifier !== '*') {
+ value = value.substring(0, parseInt(modifier, 10));
+ }
+
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+ } else {
+ if (modifier === '*') {
+ if (Array.isArray(value)) {
+ value.filter(isDefined).forEach(function (value) {
+ result.push(encodeValue(operator, value, isKeyOperator(operator) ? key : null));
+ });
+ } else {
+ Object.keys(value).forEach(function (k) {
+ if (isDefined(value[k])) {
+ result.push(encodeValue(operator, value[k], k));
+ }
+ });
+ }
+ } else {
+ var tmp = [];
+
+ if (Array.isArray(value)) {
+ value.filter(isDefined).forEach(function (value) {
+ tmp.push(encodeValue(operator, value));
+ });
+ } else {
+ Object.keys(value).forEach(function (k) {
+ if (isDefined(value[k])) {
+ tmp.push(encodeURIComponent(k));
+ tmp.push(encodeValue(operator, value[k].toString()));
+ }
+ });
+ }
+
+ if (isKeyOperator(operator)) {
+ result.push(encodeURIComponent(key) + '=' + tmp.join(','));
+ } else if (tmp.length !== 0) {
+ result.push(tmp.join(','));
+ }
+ }
+ }
+ } else {
+ if (operator === ';') {
+ result.push(encodeURIComponent(key));
+ } else if (value === '' && (operator === '&' || operator === '?')) {
+ result.push(encodeURIComponent(key) + '=');
+ } else if (value === '') {
+ result.push('');
+ }
+ }
+
+ return result;
+ }
+
+ function isDefined(value) {
+ return value !== undefined && value !== null;
+ }
+
+ function isKeyOperator(operator) {
+ return operator === ';' || operator === '&' || operator === '?';
+ }
+
+ function encodeValue(operator, value, key) {
+
+ value = operator === '+' || operator === '#' ? encodeReserved(value) : encodeURIComponent(value);
+
+ if (key) {
+ return encodeURIComponent(key) + '=' + value;
+ } else {
+ return value;
+ }
+ }
+
+ function encodeReserved(str) {
+ return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) {
+ if (!/%[0-9A-Fa-f]/.test(part)) {
+ part = encodeURI(part);
+ }
+ return part;
+ }).join('');
+ }
+
+ function template (options) {
+
+ var variables = [],
+ url = expand(options.url, options.params, variables);
+
+ variables.forEach(function (key) {
+ delete options.params[key];
+ });
+
+ return url;
+ }
+
+ /**
+ * Service for URL templating.
+ */
+
+ var ie = document.documentMode;
+ var el = document.createElement('a');
+
+ function Url(url, params) {
+
+ var self = this || {},
+ options = url,
+ transform;
+
+ if (isString(url)) {
+ options = { url: url, params: params };
+ }
+
+ options = merge({}, Url.options, self.$options, options);
+
+ Url.transforms.forEach(function (handler) {
+ transform = factory(handler, transform, self.$vm);
+ });
+
+ return transform(options);
+ }
+
+ /**
+ * Url options.
+ */
+
+ Url.options = {
+ url: '',
+ root: null,
+ params: {}
+ };
+
+ /**
+ * Url transforms.
+ */
+
+ Url.transforms = [template, query, root];
+
+ /**
+ * Encodes a Url parameter string.
+ *
+ * @param {Object} obj
+ */
+
+ Url.params = function (obj) {
+
+ var params = [],
+ escape = encodeURIComponent;
+
+ params.add = function (key, value) {
+
+ if (isFunction(value)) {
+ value = value();
+ }
+
+ if (value === null) {
+ value = '';
+ }
+
+ this.push(escape(key) + '=' + escape(value));
+ };
+
+ serialize(params, obj);
+
+ return params.join('&').replace(/%20/g, '+');
+ };
+
+ /**
+ * Parse a URL and return its components.
+ *
+ * @param {String} url
+ */
+
+ Url.parse = function (url) {
+
+ if (ie) {
+ el.href = url;
+ url = el.href;
+ }
+
+ el.href = url;
+
+ return {
+ href: el.href,
+ protocol: el.protocol ? el.protocol.replace(/:$/, '') : '',
+ port: el.port,
+ host: el.host,
+ hostname: el.hostname,
+ pathname: el.pathname.charAt(0) === '/' ? el.pathname : '/' + el.pathname,
+ search: el.search ? el.search.replace(/^\?/, '') : '',
+ hash: el.hash ? el.hash.replace(/^#/, '') : ''
+ };
+ };
+
+ function factory(handler, next, vm) {
+ return function (options) {
+ return handler.call(vm, options, next);
+ };
+ }
+
+ function serialize(params, obj, scope) {
+
+ var array = isArray(obj),
+ plain = isPlainObject(obj),
+ hash;
+
+ each(obj, function (value, key) {
+
+ hash = isObject(value) || isArray(value);
+
+ if (scope) {
+ key = scope + '[' + (plain || hash ? key : '') + ']';
+ }
+
+ if (!scope && array) {
+ params.add(value.name, value.value);
+ } else if (hash) {
+ serialize(params, value, key);
+ } else {
+ params.add(key, value);
+ }
+ });
+ }
+
+ function xdrClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var xdr = new XDomainRequest(),
+ handler = function (event) {
+
+ var response = request.respondWith(xdr.responseText, {
+ status: xdr.status,
+ statusText: xdr.statusText
+ });
+
+ resolve(response);
+ };
+
+ request.abort = function () {
+ return xdr.abort();
+ };
+
+ xdr.open(request.method, request.getUrl(), true);
+ xdr.timeout = 0;
+ xdr.onload = handler;
+ xdr.onerror = handler;
+ xdr.ontimeout = function () {};
+ xdr.onprogress = function () {};
+ xdr.send(request.getBody());
+ });
+ }
+
+ var ORIGIN_URL = Url.parse(location.href);
+ var SUPPORTS_CORS = 'withCredentials' in new XMLHttpRequest();
+
+ function cors (request, next) {
+
+ if (!isBoolean(request.crossOrigin) && crossOrigin(request)) {
+ request.crossOrigin = true;
+ }
+
+ if (request.crossOrigin) {
+
+ if (!SUPPORTS_CORS) {
+ request.client = xdrClient;
+ }
+
+ delete request.emulateHTTP;
+ }
+
+ next();
+ }
+
+ function crossOrigin(request) {
+
+ var requestUrl = Url.parse(Url(request));
+
+ return requestUrl.protocol !== ORIGIN_URL.protocol || requestUrl.host !== ORIGIN_URL.host;
+ }
+
+ function body (request, next) {
+
+ if (request.emulateJSON && isPlainObject(request.body)) {
+ request.body = Url.params(request.body);
+ request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
+ if (isFormData(request.body)) {
+ delete request.headers['Content-Type'];
+ }
+
+ if (isPlainObject(request.body)) {
+ request.body = JSON.stringify(request.body);
+ }
+
+ next(function (response) {
+
+ var contentType = response.headers['Content-Type'];
+
+ if (isString(contentType) && contentType.indexOf('application/json') === 0) {
+
+ try {
+ response.data = response.json();
+ } catch (e) {
+ response.data = null;
+ }
+ } else {
+ response.data = response.text();
+ }
+ });
+ }
+
+ function jsonpClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var name = request.jsonp || 'callback',
+ callback = '_jsonp' + Math.random().toString(36).substr(2),
+ body = null,
+ handler,
+ script;
+
+ handler = function (event) {
+
+ var status = 0;
+
+ if (event.type === 'load' && body !== null) {
+ status = 200;
+ } else if (event.type === 'error') {
+ status = 404;
+ }
+
+ resolve(request.respondWith(body, { status: status }));
+
+ delete window[callback];
+ document.body.removeChild(script);
+ };
+
+ request.params[name] = callback;
+
+ window[callback] = function (result) {
+ body = JSON.stringify(result);
+ };
+
+ script = document.createElement('script');
+ script.src = request.getUrl();
+ script.type = 'text/javascript';
+ script.async = true;
+ script.onload = handler;
+ script.onerror = handler;
+
+ document.body.appendChild(script);
+ });
+ }
+
+ function jsonp (request, next) {
+
+ if (request.method == 'JSONP') {
+ request.client = jsonpClient;
+ }
+
+ next(function (response) {
+
+ if (request.method == 'JSONP') {
+ response.data = response.json();
+ }
+ });
+ }
+
+ function before (request, next) {
+
+ if (isFunction(request.before)) {
+ request.before.call(this, request);
+ }
+
+ next();
+ }
+
+ /**
+ * HTTP method override Interceptor.
+ */
+
+ function method (request, next) {
+
+ if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) {
+ request.headers['X-HTTP-Method-Override'] = request.method;
+ request.method = 'POST';
+ }
+
+ next();
+ }
+
+ function header (request, next) {
+
+ request.method = request.method.toUpperCase();
+ request.headers = assign({}, Http.headers.common, !request.crossOrigin ? Http.headers.custom : {}, Http.headers[request.method.toLowerCase()], request.headers);
+
+ next();
+ }
+
+ /**
+ * Timeout Interceptor.
+ */
+
+ function timeout (request, next) {
+
+ var timeout;
+
+ if (request.timeout) {
+ timeout = setTimeout(function () {
+ request.abort();
+ }, request.timeout);
+ }
+
+ next(function (response) {
+
+ clearTimeout(timeout);
+ });
+ }
+
+ function xhrClient (request) {
+ return new Promise$1(function (resolve) {
+
+ var xhr = new XMLHttpRequest(),
+ handler = function (event) {
+
+ var response = request.respondWith('response' in xhr ? xhr.response : xhr.responseText, {
+ status: xhr.status === 1223 ? 204 : xhr.status, // IE9 status bug
+ statusText: xhr.status === 1223 ? 'No Content' : trim(xhr.statusText),
+ headers: parseHeaders(xhr.getAllResponseHeaders())
+ });
+
+ resolve(response);
+ };
+
+ request.abort = function () {
+ return xhr.abort();
+ };
+
+ xhr.open(request.method, request.getUrl(), true);
+ xhr.timeout = 0;
+ xhr.onload = handler;
+ xhr.onerror = handler;
+
+ if (request.progress) {
+ if (request.method === 'GET') {
+ xhr.addEventListener('progress', request.progress);
+ } else if (/^(POST|PUT)$/i.test(request.method)) {
+ xhr.upload.addEventListener('progress', request.progress);
+ }
+ }
+
+ if (request.credentials === true) {
+ xhr.withCredentials = true;
+ }
+
+ each(request.headers || {}, function (value, header) {
+ xhr.setRequestHeader(header, value);
+ });
+
+ xhr.send(request.getBody());
+ });
+ }
+
+ function parseHeaders(str) {
+
+ var headers = {},
+ value,
+ name,
+ i;
+
+ each(trim(str).split('\n'), function (row) {
+
+ i = row.indexOf(':');
+ name = trim(row.slice(0, i));
+ value = trim(row.slice(i + 1));
+
+ if (headers[name]) {
+
+ if (isArray(headers[name])) {
+ headers[name].push(value);
+ } else {
+ headers[name] = [headers[name], value];
+ }
+ } else {
+
+ headers[name] = value;
+ }
+ });
+
+ return headers;
+ }
+
+ function Client (context) {
+
+ var reqHandlers = [sendRequest],
+ resHandlers = [],
+ handler;
+
+ if (!isObject(context)) {
+ context = null;
+ }
+
+ function Client(request) {
+ return new Promise$1(function (resolve) {
+
+ function exec() {
+
+ handler = reqHandlers.pop();
+
+ if (isFunction(handler)) {
+ handler.call(context, request, next);
+ } else {
+ warn('Invalid interceptor of type ' + typeof handler + ', must be a function');
+ next();
+ }
+ }
+
+ function next(response) {
+
+ if (isFunction(response)) {
+
+ resHandlers.unshift(response);
+ } else if (isObject(response)) {
+
+ resHandlers.forEach(function (handler) {
+ response = when(response, function (response) {
+ return handler.call(context, response) || response;
+ });
+ });
+
+ when(response, resolve);
+
+ return;
+ }
+
+ exec();
+ }
+
+ exec();
+ }, context);
+ }
+
+ Client.use = function (handler) {
+ reqHandlers.push(handler);
+ };
+
+ return Client;
+ }
+
+ function sendRequest(request, resolve) {
+
+ var client = request.client || xhrClient;
+
+ resolve(client(request));
+ }
+
+ var classCallCheck = function (instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ };
+
+ /**
+ * HTTP Response.
+ */
+
+ var Response = function () {
+ function Response(body, _ref) {
+ var url = _ref.url;
+ var headers = _ref.headers;
+ var status = _ref.status;
+ var statusText = _ref.statusText;
+ classCallCheck(this, Response);
+
+
+ this.url = url;
+ this.body = body;
+ this.headers = headers || {};
+ this.status = status || 0;
+ this.statusText = statusText || '';
+ this.ok = status >= 200 && status < 300;
+ }
+
+ Response.prototype.text = function text() {
+ return this.body;
+ };
+
+ Response.prototype.blob = function blob() {
+ return new Blob([this.body]);
+ };
+
+ Response.prototype.json = function json() {
+ return JSON.parse(this.body);
+ };
+
+ return Response;
+ }();
+
+ var Request = function () {
+ function Request(options) {
+ classCallCheck(this, Request);
+
+
+ this.method = 'GET';
+ this.body = null;
+ this.params = {};
+ this.headers = {};
+
+ assign(this, options);
+ }
+
+ Request.prototype.getUrl = function getUrl() {
+ return Url(this);
+ };
+
+ Request.prototype.getBody = function getBody() {
+ return this.body;
+ };
+
+ Request.prototype.respondWith = function respondWith(body, options) {
+ return new Response(body, assign(options || {}, { url: this.getUrl() }));
+ };
+
+ return Request;
+ }();
+
+ /**
+ * Service for sending network requests.
+ */
+
+ var CUSTOM_HEADERS = { 'X-Requested-With': 'XMLHttpRequest' };
+ var COMMON_HEADERS = { 'Accept': 'application/json, text/plain, */*' };
+ var JSON_CONTENT_TYPE = { 'Content-Type': 'application/json;charset=utf-8' };
+
+ function Http(options) {
+
+ var self = this || {},
+ client = Client(self.$vm);
+
+ defaults(options || {}, self.$options, Http.options);
+
+ Http.interceptors.forEach(function (handler) {
+ client.use(handler);
+ });
+
+ return client(new Request(options)).then(function (response) {
+
+ return response.ok ? response : Promise$1.reject(response);
+ }, function (response) {
+
+ if (response instanceof Error) {
+ error(response);
+ }
+
+ return Promise$1.reject(response);
+ });
+ }
+
+ Http.options = {};
+
+ Http.headers = {
+ put: JSON_CONTENT_TYPE,
+ post: JSON_CONTENT_TYPE,
+ patch: JSON_CONTENT_TYPE,
+ delete: JSON_CONTENT_TYPE,
+ custom: CUSTOM_HEADERS,
+ common: COMMON_HEADERS
+ };
+
+ Http.interceptors = [before, timeout, method, body, jsonp, header, cors];
+
+ ['get', 'delete', 'head', 'jsonp'].forEach(function (method) {
+
+ Http[method] = function (url, options) {
+ return this(assign(options || {}, { url: url, method: method }));
+ };
+ });
+
+ ['post', 'put', 'patch'].forEach(function (method) {
+
+ Http[method] = function (url, body, options) {
+ return this(assign(options || {}, { url: url, method: method, body: body }));
+ };
+ });
+
+ function Resource(url, params, actions, options) {
+
+ var self = this || {},
+ resource = {};
+
+ actions = assign({}, Resource.actions, actions);
+
+ each(actions, function (action, name) {
+
+ action = merge({ url: url, params: params || {} }, options, action);
+
+ resource[name] = function () {
+ return (self.$http || Http)(opts(action, arguments));
+ };
+ });
+
+ return resource;
+ }
+
+ function opts(action, args) {
+
+ var options = assign({}, action),
+ params = {},
+ body;
+
+ switch (args.length) {
+
+ case 2:
+
+ params = args[0];
+ body = args[1];
+
+ break;
+
+ case 1:
+
+ if (/^(POST|PUT|PATCH)$/i.test(options.method)) {
+ body = args[0];
+ } else {
+ params = args[0];
+ }
+
+ break;
+
+ case 0:
+
+ break;
+
+ default:
+
+ throw 'Expected up to 4 arguments [params, body], got ' + args.length + ' arguments';
+ }
+
+ options.body = body;
+ options.params = assign({}, options.params, params);
+
+ return options;
+ }
+
+ Resource.actions = {
+
+ get: { method: 'GET' },
+ save: { method: 'POST' },
+ query: { method: 'GET' },
+ update: { method: 'PUT' },
+ remove: { method: 'DELETE' },
+ delete: { method: 'DELETE' }
+
+ };
+
+ function plugin(Vue) {
+
+ if (plugin.installed) {
+ return;
+ }
+
+ Util(Vue);
+
+ Vue.url = Url;
+ Vue.http = Http;
+ Vue.resource = Resource;
+ Vue.Promise = Promise$1;
+
+ Object.defineProperties(Vue.prototype, {
+
+ $url: {
+ get: function () {
+ return options(Vue.url, this, this.$options.url);
+ }
+ },
+
+ $http: {
+ get: function () {
+ return options(Vue.http, this, this.$options.http);
+ }
+ },
+
+ $resource: {
+ get: function () {
+ return Vue.resource.bind(this);
+ }
+ },
+
+ $promise: {
+ get: function () {
+ var _this = this;
+
+ return function (executor) {
+ return new Vue.Promise(executor, _this);
+ };
+ }
+ }
+
+ });
+ }
+
+ if (typeof window !== 'undefined' && window.Vue) {
+ window.Vue.use(plugin);
+ }
+
+ return plugin;
+
+})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue-resource.js.erb b/vendor/assets/javascripts/vue-resource.js.erb
new file mode 100644
index 00000000000..8001775ce98
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue-resource.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue-resource.min.js b/vendor/assets/javascripts/vue-resource.min.js
new file mode 100644
index 00000000000..6bff73a2a67
--- /dev/null
+++ b/vendor/assets/javascripts/vue-resource.min.js
@@ -0,0 +1,7 @@
+/*!
+ * vue-resource v0.9.3
+ * https://github.com/vuejs/vue-resource
+ * Released under the MIT License.
+ */
+
+!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):t.VueResource=n()}(this,function(){"use strict";function t(t){this.state=Z,this.value=void 0,this.deferred=[];var n=this;try{t(function(t){n.resolve(t)},function(t){n.reject(t)})}catch(e){n.reject(e)}}function n(t,n){t instanceof nt?this.promise=t:this.promise=new nt(t.bind(n)),this.context=n}function e(t){rt=t.util,ot=t.config.debug||!t.config.silent}function o(t){"undefined"!=typeof console&&ot&&console.warn("[VueResource warn]: "+t)}function r(t){"undefined"!=typeof console&&console.error(t)}function i(t,n){return rt.nextTick(t,n)}function u(t){return t.replace(/^\s*|\s*$/g,"")}function s(t){return"string"==typeof t}function c(t){return t===!0||t===!1}function a(t){return"function"==typeof t}function f(t){return null!==t&&"object"==typeof t}function h(t){return f(t)&&Object.getPrototypeOf(t)==Object.prototype}function p(t){return"undefined"!=typeof FormData&&t instanceof FormData}function l(t,e,o){var r=n.resolve(t);return arguments.length<2?r:r.then(e,o)}function d(t,n,e){return e=e||{},a(e)&&(e=e.call(n)),v(t.bind({$vm:n,$options:e}),t,{$options:e})}function m(t,n){var e,o;if("number"==typeof t.length)for(e=0;e<t.length;e++)n.call(t[e],t[e],e);else if(f(t))for(o in t)t.hasOwnProperty(o)&&n.call(t[o],t[o],o);return t}function v(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n,!0)}),t}function y(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){for(var e in n)void 0===t[e]&&(t[e]=n[e])}),t}function b(t){var n=it.slice.call(arguments,1);return n.forEach(function(n){g(t,n)}),t}function g(t,n,e){for(var o in n)e&&(h(n[o])||ut(n[o]))?(h(n[o])&&!h(t[o])&&(t[o]={}),ut(n[o])&&!ut(t[o])&&(t[o]=[]),g(t[o],n[o],e)):void 0!==n[o]&&(t[o]=n[o])}function w(t,n){var e=n(t);return s(t.root)&&!e.match(/^(https?:)?\//)&&(e=t.root+"/"+e),e}function T(t,n){var e=Object.keys(R.options.params),o={},r=n(t);return m(t.params,function(t,n){e.indexOf(n)===-1&&(o[n]=t)}),o=R.params(o),o&&(r+=(r.indexOf("?")==-1?"?":"&")+o),r}function j(t,n,e){var o=E(t),r=o.expand(n);return e&&e.push.apply(e,o.vars),r}function E(t){var n=["+","#",".","/",";","?","&"],e=[];return{vars:e,expand:function(o){return t.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g,function(t,r,i){if(r){var u=null,s=[];if(n.indexOf(r.charAt(0))!==-1&&(u=r.charAt(0),r=r.substr(1)),r.split(/,/g).forEach(function(t){var n=/([^:\*]*)(?::(\d+)|(\*))?/.exec(t);s.push.apply(s,x(o,u,n[1],n[2]||n[3])),e.push(n[1])}),u&&"+"!==u){var c=",";return"?"===u?c="&":"#"!==u&&(c=u),(0!==s.length?u:"")+s.join(c)}return s.join(",")}return $(i)})}}}function x(t,n,e,o){var r=t[e],i=[];if(O(r)&&""!==r)if("string"==typeof r||"number"==typeof r||"boolean"==typeof r)r=r.toString(),o&&"*"!==o&&(r=r.substring(0,parseInt(o,10))),i.push(C(n,r,P(n)?e:null));else if("*"===o)Array.isArray(r)?r.filter(O).forEach(function(t){i.push(C(n,t,P(n)?e:null))}):Object.keys(r).forEach(function(t){O(r[t])&&i.push(C(n,r[t],t))});else{var u=[];Array.isArray(r)?r.filter(O).forEach(function(t){u.push(C(n,t))}):Object.keys(r).forEach(function(t){O(r[t])&&(u.push(encodeURIComponent(t)),u.push(C(n,r[t].toString())))}),P(n)?i.push(encodeURIComponent(e)+"="+u.join(",")):0!==u.length&&i.push(u.join(","))}else";"===n?i.push(encodeURIComponent(e)):""!==r||"&"!==n&&"?"!==n?""===r&&i.push(""):i.push(encodeURIComponent(e)+"=");return i}function O(t){return void 0!==t&&null!==t}function P(t){return";"===t||"&"===t||"?"===t}function C(t,n,e){return n="+"===t||"#"===t?$(n):encodeURIComponent(n),e?encodeURIComponent(e)+"="+n:n}function $(t){return t.split(/(%[0-9A-Fa-f]{2})/g).map(function(t){return/%[0-9A-Fa-f]/.test(t)||(t=encodeURI(t)),t}).join("")}function U(t){var n=[],e=j(t.url,t.params,n);return n.forEach(function(n){delete t.params[n]}),e}function R(t,n){var e,o=this||{},r=t;return s(t)&&(r={url:t,params:n}),r=v({},R.options,o.$options,r),R.transforms.forEach(function(t){e=A(t,e,o.$vm)}),e(r)}function A(t,n,e){return function(o){return t.call(e,o,n)}}function S(t,n,e){var o,r=ut(n),i=h(n);m(n,function(n,u){o=f(n)||ut(n),e&&(u=e+"["+(i||o?u:"")+"]"),!e&&r?t.add(n.name,n.value):o?S(t,n,u):t.add(u,n)})}function k(t){return new n(function(n){var e=new XDomainRequest,o=function(o){var r=t.respondWith(e.responseText,{status:e.status,statusText:e.statusText});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,e.ontimeout=function(){},e.onprogress=function(){},e.send(t.getBody())})}function H(t,n){!c(t.crossOrigin)&&I(t)&&(t.crossOrigin=!0),t.crossOrigin&&(ht||(t.client=k),delete t.emulateHTTP),n()}function I(t){var n=R.parse(R(t));return n.protocol!==ft.protocol||n.host!==ft.host}function L(t,n){t.emulateJSON&&h(t.body)&&(t.body=R.params(t.body),t.headers["Content-Type"]="application/x-www-form-urlencoded"),p(t.body)&&delete t.headers["Content-Type"],h(t.body)&&(t.body=JSON.stringify(t.body)),n(function(t){var n=t.headers["Content-Type"];if(s(n)&&0===n.indexOf("application/json"))try{t.data=t.json()}catch(e){t.data=null}else t.data=t.text()})}function q(t){return new n(function(n){var e,o,r=t.jsonp||"callback",i="_jsonp"+Math.random().toString(36).substr(2),u=null;e=function(e){var r=0;"load"===e.type&&null!==u?r=200:"error"===e.type&&(r=404),n(t.respondWith(u,{status:r})),delete window[i],document.body.removeChild(o)},t.params[r]=i,window[i]=function(t){u=JSON.stringify(t)},o=document.createElement("script"),o.src=t.getUrl(),o.type="text/javascript",o.async=!0,o.onload=e,o.onerror=e,document.body.appendChild(o)})}function N(t,n){"JSONP"==t.method&&(t.client=q),n(function(n){"JSONP"==t.method&&(n.data=n.json())})}function D(t,n){a(t.before)&&t.before.call(this,t),n()}function J(t,n){t.emulateHTTP&&/^(PUT|PATCH|DELETE)$/i.test(t.method)&&(t.headers["X-HTTP-Method-Override"]=t.method,t.method="POST"),n()}function M(t,n){t.method=t.method.toUpperCase(),t.headers=st({},V.headers.common,t.crossOrigin?{}:V.headers.custom,V.headers[t.method.toLowerCase()],t.headers),n()}function X(t,n){var e;t.timeout&&(e=setTimeout(function(){t.abort()},t.timeout)),n(function(t){clearTimeout(e)})}function W(t){return new n(function(n){var e=new XMLHttpRequest,o=function(o){var r=t.respondWith("response"in e?e.response:e.responseText,{status:1223===e.status?204:e.status,statusText:1223===e.status?"No Content":u(e.statusText),headers:B(e.getAllResponseHeaders())});n(r)};t.abort=function(){return e.abort()},e.open(t.method,t.getUrl(),!0),e.timeout=0,e.onload=o,e.onerror=o,t.progress&&("GET"===t.method?e.addEventListener("progress",t.progress):/^(POST|PUT)$/i.test(t.method)&&e.upload.addEventListener("progress",t.progress)),t.credentials===!0&&(e.withCredentials=!0),m(t.headers||{},function(t,n){e.setRequestHeader(n,t)}),e.send(t.getBody())})}function B(t){var n,e,o,r={};return m(u(t).split("\n"),function(t){o=t.indexOf(":"),e=u(t.slice(0,o)),n=u(t.slice(o+1)),r[e]?ut(r[e])?r[e].push(n):r[e]=[r[e],n]:r[e]=n}),r}function F(t){function e(e){return new n(function(n){function s(){r=i.pop(),a(r)?r.call(t,e,c):(o("Invalid interceptor of type "+typeof r+", must be a function"),c())}function c(e){if(a(e))u.unshift(e);else if(f(e))return u.forEach(function(n){e=l(e,function(e){return n.call(t,e)||e})}),void l(e,n);s()}s()},t)}var r,i=[G],u=[];return f(t)||(t=null),e.use=function(t){i.push(t)},e}function G(t,n){var e=t.client||W;n(e(t))}function V(t){var e=this||{},o=F(e.$vm);return y(t||{},e.$options,V.options),V.interceptors.forEach(function(t){o.use(t)}),o(new dt(t)).then(function(t){return t.ok?t:n.reject(t)},function(t){return t instanceof Error&&r(t),n.reject(t)})}function _(t,n,e,o){var r=this||{},i={};return e=st({},_.actions,e),m(e,function(e,u){e=v({url:t,params:n||{}},o,e),i[u]=function(){return(r.$http||V)(z(e,arguments))}}),i}function z(t,n){var e,o=st({},t),r={};switch(n.length){case 2:r=n[0],e=n[1];break;case 1:/^(POST|PUT|PATCH)$/i.test(o.method)?e=n[0]:r=n[0];break;case 0:break;default:throw"Expected up to 4 arguments [params, body], got "+n.length+" arguments"}return o.body=e,o.params=st({},o.params,r),o}function K(t){K.installed||(e(t),t.url=R,t.http=V,t.resource=_,t.Promise=n,Object.defineProperties(t.prototype,{$url:{get:function(){return d(t.url,this,this.$options.url)}},$http:{get:function(){return d(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}},$promise:{get:function(){var n=this;return function(e){return new t.Promise(e,n)}}}}))}var Q=0,Y=1,Z=2;t.reject=function(n){return new t(function(t,e){e(n)})},t.resolve=function(n){return new t(function(t,e){t(n)})},t.all=function(n){return new t(function(e,o){function r(t){return function(o){u[t]=o,i+=1,i===n.length&&e(u)}}var i=0,u=[];0===n.length&&e(u);for(var s=0;s<n.length;s+=1)t.resolve(n[s]).then(r(s),o)})},t.race=function(n){return new t(function(e,o){for(var r=0;r<n.length;r+=1)t.resolve(n[r]).then(e,o)})};var tt=t.prototype;tt.resolve=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");var e=!1;try{var o=t&&t.then;if(null!==t&&"object"==typeof t&&"function"==typeof o)return void o.call(t,function(t){e||n.resolve(t),e=!0},function(t){e||n.reject(t),e=!0})}catch(r){return void(e||n.reject(r))}n.state=Q,n.value=t,n.notify()}},tt.reject=function(t){var n=this;if(n.state===Z){if(t===n)throw new TypeError("Promise settled with itself.");n.state=Y,n.value=t,n.notify()}},tt.notify=function(){var t=this;i(function(){if(t.state!==Z)for(;t.deferred.length;){var n=t.deferred.shift(),e=n[0],o=n[1],r=n[2],i=n[3];try{t.state===Q?r("function"==typeof e?e.call(void 0,t.value):t.value):t.state===Y&&("function"==typeof o?r(o.call(void 0,t.value)):i(t.value))}catch(u){i(u)}}})},tt.then=function(n,e){var o=this;return new t(function(t,r){o.deferred.push([n,e,t,r]),o.notify()})},tt["catch"]=function(t){return this.then(void 0,t)};var nt=window.Promise||t;n.all=function(t,e){return new n(nt.all(t),e)},n.resolve=function(t,e){return new n(nt.resolve(t),e)},n.reject=function(t,e){return new n(nt.reject(t),e)},n.race=function(t,e){return new n(nt.race(t),e)};var et=n.prototype;et.bind=function(t){return this.context=t,this},et.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),new n(this.promise.then(t,e),this.context)},et["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),new n(this.promise["catch"](t),this.context)},et["finally"]=function(t){return this.then(function(n){return t.call(this),n},function(n){return t.call(this),nt.reject(n)})};var ot=!1,rt={},it=[],ut=Array.isArray,st=Object.assign||b,ct=document.documentMode,at=document.createElement("a");R.options={url:"",root:null,params:{}},R.transforms=[U,T,w],R.params=function(t){var n=[],e=encodeURIComponent;return n.add=function(t,n){a(n)&&(n=n()),null===n&&(n=""),this.push(e(t)+"="+e(n))},S(n,t),n.join("&").replace(/%20/g,"+")},R.parse=function(t){return ct&&(at.href=t,t=at.href),at.href=t,{href:at.href,protocol:at.protocol?at.protocol.replace(/:$/,""):"",port:at.port,host:at.host,hostname:at.hostname,pathname:"/"===at.pathname.charAt(0)?at.pathname:"/"+at.pathname,search:at.search?at.search.replace(/^\?/,""):"",hash:at.hash?at.hash.replace(/^#/,""):""}};var ft=R.parse(location.href),ht="withCredentials"in new XMLHttpRequest,pt=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},lt=function(){function t(n,e){var o=e.url,r=e.headers,i=e.status,u=e.statusText;pt(this,t),this.url=o,this.body=n,this.headers=r||{},this.status=i||0,this.statusText=u||"",this.ok=i>=200&&i<300}return t.prototype.text=function(){return this.body},t.prototype.blob=function(){return new Blob([this.body])},t.prototype.json=function(){return JSON.parse(this.body)},t}(),dt=function(){function t(n){pt(this,t),this.method="GET",this.body=null,this.params={},this.headers={},st(this,n)}return t.prototype.getUrl=function(){return R(this)},t.prototype.getBody=function(){return this.body},t.prototype.respondWith=function(t,n){return new lt(t,st(n||{},{url:this.getUrl()}))},t}(),mt={"X-Requested-With":"XMLHttpRequest"},vt={Accept:"application/json, text/plain, */*"},yt={"Content-Type":"application/json;charset=utf-8"};return V.options={},V.headers={put:yt,post:yt,patch:yt,"delete":yt,custom:mt,common:vt},V.interceptors=[D,X,J,L,N,M,H],["get","delete","head","jsonp"].forEach(function(t){V[t]=function(n,e){return this(st(e||{},{url:n,method:t}))}}),["post","put","patch"].forEach(function(t){V[t]=function(n,e,o){return this(st(o||{},{url:n,method:t,body:e}))}}),_.actions={get:{method:"GET"},save:{method:"POST"},query:{method:"GET"},update:{method:"PUT"},remove:{method:"DELETE"},"delete":{method:"DELETE"}},"undefined"!=typeof window&&window.Vue&&window.Vue.use(K),K}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.full.js b/vendor/assets/javascripts/vue.full.js
new file mode 100644
index 00000000000..7ae95897a01
--- /dev/null
+++ b/vendor/assets/javascripts/vue.full.js
@@ -0,0 +1,10073 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 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';
+
+ 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
+ */
+
+ 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();
+ }
+ }
+ }
+
+ var hasOwnProperty = Object.prototype.hasOwnProperty;
+ /**
+ * Check whether the object has the property.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @return {Boolean}
+ */
+
+ function hasOwn(obj, key) {
+ return hasOwnProperty.call(obj, key);
+ }
+
+ /**
+ * Check if an expression is a literal value.
+ *
+ * @param {String} exp
+ * @return {Boolean}
+ */
+
+ var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/;
+
+ function isLiteral(exp) {
+ return literalValueRE.test(exp);
+ }
+
+ /**
+ * Check if a string starts with $ or _
+ *
+ * @param {String} str
+ * @return {Boolean}
+ */
+
+ function isReserved(str) {
+ var c = (str + '').charCodeAt(0);
+ return c === 0x24 || c === 0x5F;
+ }
+
+ /**
+ * Guard text output, make sure undefined outputs
+ * empty string
+ *
+ * @param {*} value
+ * @return {String}
+ */
+
+ function _toString(value) {
+ return value == null ? '' : value.toString();
+ }
+
+ /**
+ * Check and convert possible numeric strings to numbers
+ * before setting back to data
+ *
+ * @param {*} value
+ * @return {*|Number}
+ */
+
+ function toNumber(value) {
+ if (typeof value !== 'string') {
+ return value;
+ } else {
+ var parsed = Number(value);
+ return isNaN(parsed) ? value : parsed;
+ }
+ }
+
+ /**
+ * Convert string boolean literals into real booleans.
+ *
+ * @param {*} value
+ * @return {*|Boolean}
+ */
+
+ function toBoolean(value) {
+ return value === 'true' ? true : value === 'false' ? false : value;
+ }
+
+ /**
+ * Strip quotes from a string
+ *
+ * @param {String} str
+ * @return {String | false}
+ */
+
+ 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;
+ }
+
+ /**
+ * Camelize a hyphen-delmited string.
+ *
+ * @param {String} str
+ * @return {String}
+ */
+
+ var camelizeRE = /-(\w)/g;
+
+ function camelize(str) {
+ return str.replace(camelizeRE, toUpper);
+ }
+
+ function toUpper(_, c) {
+ return c ? c.toUpperCase() : '';
+ }
+
+ /**
+ * Hyphenate a camelCase string.
+ *
+ * @param {String} str
+ * @return {String}
+ */
+
+ var hyphenateRE = /([a-z\d])([A-Z])/g;
+
+ function hyphenate(str) {
+ return str.replace(hyphenateRE, '$1-$2').toLowerCase();
+ }
+
+ /**
+ * 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}
+ */
+
+ var classifyRE = /(?:^|[-_\/])(\w)/g;
+
+ function classify(str) {
+ return str.replace(classifyRE, toUpper);
+ }
+
+ /**
+ * Simple bind, faster than native
+ *
+ * @param {Function} fn
+ * @param {Object} ctx
+ * @return {Function}
+ */
+
+ 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);
+ };
+ }
+
+ /**
+ * Convert an Array-like object to a real Array.
+ *
+ * @param {Array-like} list
+ * @param {Number} [start] - start index
+ * @return {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.
+ *
+ * @param {Object} to
+ * @param {Object} from
+ */
+
+ function extend(to, from) {
+ var keys = Object.keys(from);
+ var i = keys.length;
+ while (i--) {
+ to[keys[i]] = from[keys[i]];
+ }
+ return to;
+ }
+
+ /**
+ * Quick object check - this is primarily used to tell
+ * Objects from primitive values when we know the value
+ * is a JSON-compliant type.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ function isObject(obj) {
+ return obj !== null && typeof obj === 'object';
+ }
+
+ /**
+ * Strict object type check. Only returns true
+ * for plain JavaScript objects.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ var toString = Object.prototype.toString;
+ var OBJECT_STRING = '[object Object]';
+
+ function isPlainObject(obj) {
+ return toString.call(obj) === OBJECT_STRING;
+ }
+
+ /**
+ * Array type check.
+ *
+ * @param {*} obj
+ * @return {Boolean}
+ */
+
+ var isArray = Array.isArray;
+
+ /**
+ * Define a property.
+ *
+ * @param {Object} obj
+ * @param {String} key
+ * @param {*} val
+ * @param {Boolean} [enumerable]
+ */
+
+ function def(obj, key, val, enumerable) {
+ Object.defineProperty(obj, key, {
+ value: val,
+ enumerable: !!enumerable,
+ writable: true,
+ configurable: true
+ });
+ }
+
+ /**
+ * 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
+ */
+
+ 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;
+ };
+ }
+
+ /**
+ * Manual indexOf because it's slightly faster than
+ * native.
+ *
+ * @param {Array} arr
+ * @param {*} obj
+ */
+
+ function indexOf(arr, obj) {
+ var i = arr.length;
+ while (i--) {
+ if (arr[i] === obj) return i;
+ }
+ return -1;
+ }
+
+ /**
+ * Make a cancellable version of an async callback.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ */
+
+ 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}
+ */
+
+ function looseEqual(a, b) {
+ /* eslint-disable eqeqeq */
+ return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false);
+ /* eslint-enable eqeqeq */
+ }
+
+ var hasProto = ('__proto__' in {});
+
+ // Browser environment sniffing
+ var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]';
+
+ // detect devtools
+ var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
+
+ // 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('_');
+
+ // detecting iOS UIWebView by indexedDB
+ var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB;
+
+ var transitionProp = undefined;
+ var transitionEndEvent = undefined;
+ var animationProp = undefined;
+ var animationEndEvent = undefined;
+
+ // 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. 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;
+ pending = true;
+ timerFunc(nextTickHandler, 0);
+ };
+ })();
+
+ 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 () {
+ this.set = Object.create(null);
+ };
+ _Set.prototype.has = function (key) {
+ return this.set[key] !== undefined;
+ };
+ _Set.prototype.add = function (key) {
+ this.set[key] = 1;
+ };
+ _Set.prototype.clear = function () {
+ 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;
+ }
+ this.tail = entry;
+ this.size++;
+ }
+ 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--;
+ }
+ 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+/;
+
+ /**
+ * 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
+ */
+
+ 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;
+ }
+
+ /**
+ * Check if an argument is dynamic and strip quotes.
+ *
+ * @param {String} arg
+ * @return {Object}
+ */
+
+ 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
+ };
+ }
+ }
+
+ /**
+ * 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}
+ */
+
+ 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; // }
+ }
+ }
+ }
+
+ if (dir.expression == null) {
+ dir.expression = str.slice(0, i).trim();
+ } else if (lastFilterIndex !== 0) {
+ pushFilter();
+ }
+
+ cache$1.put(s, dir);
+ return dir;
+ }
+
+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, '\\$&');
+ }
+
+ 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);
+ }
+
+ /**
+ * 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)
+ });
+ }
+ // 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 + '"';
+ }
+
+ /**
+ * 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}
+ */
+
+ var filterRE = /[^|]\|[^|]/;
+ function inlineFilters(exp, single) {
+ if (!filterRE.test(exp)) {
+ return single ? exp : '(' + exp + ')';
+ } 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?
+ }
+ }
+ }
+
+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
+ }
+ });
+
+ 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]
+ */
+
+ function beforeWithTransition(el, target, vm, cb) {
+ applyTransition(el, 1, function () {
+ before(el, target);
+ }, vm, cb);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+ }
+ 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));
+ }
+
+ /**
+ * Get and remove an attribute from a node.
+ *
+ * @param {Node} node
+ * @param {String} _attr
+ */
+
+ function getAttr(node, _attr) {
+ var val = node.getAttribute(_attr);
+ if (val !== null) {
+ node.removeAttribute(_attr);
+ }
+ return val;
+ }
+
+ /**
+ * Get an attribute with colon or v-bind: prefix.
+ *
+ * @param {Node} node
+ * @param {String} name
+ * @return {String|null}
+ */
+
+ function getBindAttr(node, name) {
+ var val = getAttr(node, ':' + name);
+ if (val === null) {
+ val = getAttr(node, 'v-bind:' + name);
+ }
+ 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);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Replace target with el
+ *
+ * @param {Element} target
+ * @param {Element} el
+ */
+
+ function replace(target, el) {
+ var parent = target.parentNode;
+ if (parent) {
+ parent.replaceChild(el, target);
+ }
+ }
+
+ /**
+ * Add event listener shorthand.
+ *
+ * @param {Element} el
+ * @param {String} event
+ * @param {Function} cb
+ * @param {Boolean} [useCapture]
+ */
+
+ function on(el, event, cb, useCapture) {
+ el.addEventListener(event, cb, useCapture);
+ }
+
+ /**
+ * Remove event listener shorthand.
+ *
+ * @param {Element} el
+ * @param {String} event
+ * @param {Function} cb
+ */
+
+ function off(el, event, cb) {
+ el.removeEventListener(event, cb);
+ }
+
+ /**
+ * For IE9 compat: when both class and :class are present
+ * getAttribute('class') returns wrong value...
+ *
+ * @param {Element} el
+ * @return {String}
+ */
+
+ function getClass(el) {
+ var classname = el.className;
+ if (typeof classname === 'object') {
+ classname = classname.baseVal || '';
+ }
+ return classname;
+ }
+
+ /**
+ * 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
+ */
+
+ function setClass(el, cls) {
+ /* istanbul ignore if */
+ if (isIE9 && !/svg$/.test(el.namespaceURI)) {
+ el.className = cls;
+ } else {
+ el.setAttribute('class', cls);
+ }
+ }
+
+ /**
+ * 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());
+ }
+ }
+ }
+
+ /**
+ * Remove class with compatibility for IE & SVG
+ *
+ * @param {Element} el
+ * @param {String} cls
+ */
+
+ function removeClass(el, cls) {
+ if (el.classList) {
+ el.classList.remove(cls);
+ } else {
+ var cur = ' ' + getClass(el) + ' ';
+ var tar = ' ' + cls + ' ';
+ while (cur.indexOf(tar) >= 0) {
+ cur = cur.replace(tar, ' ');
+ }
+ setClass(el, cur.trim());
+ }
+ if (!el.className) {
+ el.removeAttribute('class');
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+ 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);
+ }
+ /* eslint-enable no-sequences */
+ }
+
+ function isTrimmable(node) {
+ return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8);
+ }
+
+ /**
+ * 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';
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Find a component ref attribute that starts with $.
+ *
+ * @param {Element} node
+ * @return {String|undefined}
+ */
+
+ var refRE = /^v-ref:/;
+
+ 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, ''));
+ }
+ }
+ }
+ }
+
+ /**
+ * 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;
+ }
+ 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();
+ }
+ }
+ }
+
+ /**
+ * 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;
+ } 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;
+ } 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 };
+ } 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.');
+ }
+ }
+ }
+ } 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 };
+ }
+ }
+ }
+
+ /**
+ * 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);
+
+ /**
+ * 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;
+ }
+ };
+ }
+ };
+
+ /**
+ * 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;
+ }
+
+ 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.
+ */
+
+ 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];
+ }
+ 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 defaultStrat(parentVal, childVal) {
+ return childVal === undefined ? parentVal : childVal;
+ };
+
+ /**
+ * Make sure component options get converted to actual
+ * constructors.
+ *
+ * @param {Object} 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * Ensure all props option syntax are normalized into the
+ * Object-based format.
+ *
+ * @param {Object} options
+ */
+
+ 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 };
+ }
+ }
+ }
+ }
+
+ /**
+ * Guard an Array-format assets option and converted it
+ * into the key-value Object format.
+ *
+ * @param {Object|Array} assets
+ * @return {Object}
+ */
+
+ 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.');
+ } else {
+ res[id] = asset;
+ }
+ }
+ 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.');
+ }
+ }
+ var options = {};
+ var key;
+ if (child['extends']) {
+ parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm);
+ }
+ 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);
+ }
+ }
+ for (key in parent) {
+ mergeField(key);
+ }
+ for (key in child) {
+ if (!hasOwn(parent, key)) {
+ mergeField(key);
+ }
+ }
+ function mergeField(key) {
+ var strat = strats[key] || defaultStrat;
+ options[key] = strat(parent[key], child[key], vm, key);
+ }
+ return options;
+ }
+
+ /**
+ * Resolve an asset.
+ * This function is used because child instances need access
+ * to assets defined in its ancestor chain.
+ *
+ * @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;
+ }
+ 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);
+ }
+ 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();
+ }
+ };
+
+ 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;
+ }
+ 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);
+ }
+ });
+
+ 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);
+ }
+ }
+
+ // 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]]);
+ }
+ };
+
+ /**
+ * 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]);
+ }
+ };
+
+ /**
+ * 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);
+ };
+
+ // helpers
+
+ /**
+ * Augment an target Object or Array by intercepting
+ * the prototype chain using __proto__
+ *
+ * @param {Object|Array} target
+ * @param {Object} src
+ */
+
+ 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.
+ *
+ * @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]);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ return ob;
+ }
+
+ /**
+ * 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;
+ }
+
+ // 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);
+ }
+ };
+ }
+
+ 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';
+
+ 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';
+ }
+
+ // a-z, A-Z
+ if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) {
+ return 'ident';
+ }
+
+ // 1-9
+ if (code >= 0x31 && code <= 0x39) {
+ return 'number';
+ }
+
+ 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;
+ }
+ 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;
+ }
+ };
+
+ actions[INC_SUB_PATH_DEPTH] = function () {
+ actions[APPEND]();
+ subPathDepth++;
+ };
+
+ actions[PUSH_SUB_PATH] = function () {
+ if (subPathDepth > 0) {
+ subPathDepth--;
+ mode = IN_SUB_PATH;
+ actions[APPEND]();
+ } else {
+ subPathDepth = 0;
+ key = formatSubPath(key);
+ if (key === false) {
+ return false;
+ } else {
+ actions[PUSH]();
+ }
+ }
+ };
+
+ 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;
+ }
+ }
+
+ 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
+ }
+
+ mode = transition[0];
+ action = actions[transition[1]];
+ if (action) {
+ newChar = transition[2];
+ newChar = newChar === undefined ? c : newChar;
+ if (action() === false) {
+ return;
+ }
+ }
+
+ if (mode === AFTER_PATH) {
+ keys.raw = path;
+ return keys;
+ }
+ }
+ }
+
+ /**
+ * External parse that check for a cache hit first
+ *
+ * @param {String} path
+ * @return {Array|undefined}
+ */
+
+ function parsePath(path) {
+ var hit = pathCache.get(path);
+ if (!hit) {
+ hit = parse(path);
+ if (hit) {
+ pathCache.put(path, hit);
+ }
+ }
+ 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);
+ };
+ }
+
+ /**
+ * Set on an object from a path
+ *
+ * @param {Object} obj
+ * @param {String | Array} path
+ * @param {*} val
+ */
+
+ 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);
+ }
+ } 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);
+ }
+ 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}
+ */
+
+ function rewrite(raw) {
+ var c = raw.charAt(0);
+ var path = raw.slice(1);
+ if (allowedKeywordsRE.test(path)) {
+ return raw;
+ } else {
+ path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path;
+ return c + 'scope.' + path;
+ }
+ }
+
+ /**
+ * Restore replacer
+ *
+ * @param {String} str
+ * @param {String} i - matched save index
+ * @return {String}
+ */
+
+ function restore(str, i) {
+ return saved[i];
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 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;
+ }
+ }
+
+ /**
+ * Compile a setter function for the expression.
+ *
+ * @param {String} exp
+ * @return {Function|undefined}
+ */
+
+ 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);
+ }
+ }
+
+ /**
+ * Parse an expression into re-written getter/setters.
+ *
+ * @param {String} exp
+ * @param {Boolean} needSet
+ * @return {Function}
+ */
+
+ 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);
+ }
+ 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);
+ }
+ 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;
+
+ 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();
+ }
+ }
+
+ /**
+ * 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;
+ }
+ }
+ }
+ queue.length = 0;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Evaluate the getter, and re-collect dependencies.
+ */
+
+ 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;
+ };
+
+ /**
+ * Set the corresponding value with the setter.
+ *
+ * @param {*} value
+ */
+
+ 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);
+ }
+ });
+ }
+ };
+
+ /**
+ * Prepare for dependency collection.
+ */
+
+ Watcher.prototype.beforeGet = function () {
+ Dep.target = this;
+ };
+
+ /**
+ * 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);
+ }
+ }
+ };
+
+ /**
+ * 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
+ */
+
+ 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);
+ }
+ };
+
+ /**
+ * 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);
+ }
+ }
+ 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);
+ }
+ var i = this.deps.length;
+ while (i--) {
+ this.deps[i].removeSub(this);
+ }
+ 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);
+ }
+ }
+ }
+
+ var text$1 = {
+
+ bind: function bind() {
+ this.attr = this.el.nodeType === 3 ? 'data' : 'textContent';
+ },
+
+ update: function update(value) {
+ this.el[this.attr] = _toString(value);
+ }
+ };
+
+ 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);
+ }
+
+ 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);
+ }
+ }
+ 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);
+ }
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+
+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;
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Remove fragment, single node version
+ */
+
+ 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();
+ });
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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();
+ }
+ };
+
+ /**
+ * Destroy the fragment.
+ */
+
+ Fragment.prototype.destroy = function () {
+ if (this.parentFrag) {
+ this.parentFrag.childFrags.$remove(this);
+ }
+ this.node.__v_frag = null;
+ this.unlink();
+ };
+
+ /**
+ * Call attach hook for a Vue instance.
+ *
+ * @param {Vue} child
+ */
+
+ function attach(child) {
+ if (!child._isAttached && inDoc(child.$el)) {
+ child._callHook('attached');
+ }
+ }
+
+ /**
+ * Call detach hook for a Vue instance.
+ *
+ * @param {Vue} child
+ */
+
+ function detach(child) {
+ if (child._isAttached && !inDoc(child.$el)) {
+ child._callHook('detached');
+ }
+ }
+
+ var linkerCache = new Cache(5000);
+
+ /**
+ * 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
+ */
+ 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);
+ }
+ } 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 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);
+ } 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);
+ }
+ } 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);
+ }
+ 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();
+ }
+ }
+ }
+ };
+
+ /**
+ * 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;
+ }
+ }
+ return node.__vue__;
+ }
+
+ /**
+ * Create a range array from given number.
+ *
+ * @param {Number} n
+ * @return {Array}
+ */
+
+ function range(n) {
+ var i = -1;
+ var ret = new Array(Math.floor(n));
+ while (++i < n) {
+ ret[i] = i;
+ }
+ return ret;
+ }
+
+ /**
+ * Get the track by key for an item.
+ *
+ * @param {Number} index
+ * @param {String} key
+ * @param {*} value
+ * @param {String} [trackByKey]
+ */
+
+ 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);
+ };
+ }
+
+ var vIf = {
+
+ priority: IF,
+ terminal: true,
+
+ 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;
+ }
+ },
+
+ update: function update(value) {
+ if (this.invalid) return;
+ if (value) {
+ if (!this.frag) {
+ this.insert();
+ }
+ } else {
+ this.remove();
+ }
+ },
+
+ 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);
+ },
+
+ 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);
+ }
+ },
+
+ unbind: function unbind() {
+ if (this.frag) {
+ this.frag.destroy();
+ }
+ if (this.elseFrag) {
+ this.elseFrag.destroy();
+ }
+ }
+ };
+
+ var show = {
+
+ bind: function bind() {
+ // check else block
+ var next = this.el.nextElementSibling;
+ if (next && getAttr(next, 'v-else') !== null) {
+ this.elseEl = next;
+ }
+ },
+
+ update: function update(value) {
+ this.apply(this.el, value);
+ if (this.elseEl) {
+ this.apply(this.elseEl, !value);
+ }
+ },
+
+ 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 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);
+ }
+ });
+ };
+
+ // 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);
+ }
+ }
+
+ // 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();
+ }
+ });
+ }
+
+ // set initial value if present
+ if (el.hasAttribute('value') || el.tagName === 'TEXTAREA' && el.value.trim()) {
+ this.afterBind = this.listener;
+ }
+ },
+
+ 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);
+ }
+ }
+ };
+
+ 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());
+ }
+ };
+
+ var select = {
+
+ bind: function bind() {
+ var _this = this;
+
+ var self = this;
+ var el = this.el;
+
+ // method to force update DOM using latest value.
+ this.forceUpdate = function () {
+ if (self._watcher) {
+ self.update(self._watcher.get());
+ }
+ };
+
+ // 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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+ return -1;
+ }
+
+ var checkbox = {
+
+ bind: function bind() {
+ var self = this;
+ var el = this.el;
+
+ this.getValue = function () {
+ return el.hasOwnProperty('_value') ? el._value : self.params.number ? toNumber(el.value) : el.value;
+ };
+
+ function getBooleanValue() {
+ var val = el.checked;
+ if (val && el.hasOwnProperty('_trueValue')) {
+ return el._trueValue;
+ }
+ if (!val && el.hasOwnProperty('_falseValue')) {
+ return el._falseValue;
+ }
+ return val;
+ }
+
+ 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());
+ }
+ };
+
+ this.on('change', this.listener);
+ if (el.hasAttribute('checked')) {
+ this.afterBind = this.listener;
+ }
+ },
+
+ 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;
+ }
+ }
+ }
+ };
+
+ var handlers = {
+ text: text$2,
+ radio: radio,
+ select: select,
+ checkbox: checkbox
+ };
+
+ 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;
+ },
+
+ /**
+ * 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;
+ }
+ }
+ },
+
+ unbind: function unbind() {
+ this.el.__v_model = null;
+ this._unbind && this._unbind();
+ }
+ };
+
+ // keyCode aliases
+ var keyCodes = {
+ esc: 27,
+ tab: 9,
+ enter: 13,
+ space: 32,
+ 'delete': [8, 46],
+ up: 38,
+ left: 37,
+ right: 39,
+ down: 40
+ };
+
+ function keyFilter(handler, keys) {
+ var codes = keys.map(function (key) {
+ var charCode = key.charCodeAt(0);
+ if (charCode > 47 && charCode < 58) {
+ return parseInt(key, 10);
+ }
+ 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);
+ }
+ };
+ }
+
+ function stopFilter(handler) {
+ return function stopHandler(e) {
+ e.stopPropagation();
+ return handler.call(this, e);
+ };
+ }
+
+ function preventFilter(handler) {
+ return function preventHandler(e) {
+ e.preventDefault();
+ return handler.call(this, e);
+ };
+ }
+
+ function selfFilter(handler) {
+ return function selfHandler(e) {
+ if (e.target === e.currentTarget) {
+ return handler.call(this, e);
+ }
+ };
+ }
+
+ 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();
+ }
+ };
+
+ 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);
+ }
+ }
+ },
+
+ 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] = '';
+ }
+ }
+
+ };
+
+ /**
+ * 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];
+ }
+ 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
+ };
+ }
+ 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'
+ };
+
+ var bind$1 = {
+
+ priority: BIND,
+
+ bind: function bind() {
+ var attr = this.arg;
+ var tag = this.el.tagName;
+ // should be deep watch on object mode
+ if (!attr) {
+ this.deep = true;
+ }
+ // 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);
+ }
+ }
+ }
+ },
+
+ update: function update(value) {
+ if (this.invalid) {
+ return;
+ }
+ var attr = this.arg;
+ if (this.arg) {
+ this.handleSingle(attr, value);
+ } else {
+ this.handleObject(value || {});
+ }
+ },
+
+ // 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);
+ }
+ 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;
+ }
+ }
+ // 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();
+ }
+ }
+ // 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 {
+ el.setAttribute(attr, value === true ? '' : value);
+ }
+ } 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;
+ }
+ }
+ };
+
+ 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);
+ }
+ };
+
+ var cloak = {
+ bind: function bind() {
+ var el = this.el;
+ this.vm.$once('pre-hook:compiled', function () {
+ el.removeAttribute('v-cloak');
+ });
+ }
+ };
+
+ // 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));
+ }
+ },
+
+ 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);
+ }
+ }
+ 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);
+ }
+ }
+ }
+ };
+
+ /**
+ * 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);
+ } else {
+ for (var k in _key) {
+ if (_key[k]) res.push(k);
+ }
+ }
+ }
+ }
+ } else if (isObject(value)) {
+ for (var key in value) {
+ if (value[key]) res.push(key);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * 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]);
+ }
+ }
+
+ 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">
+ */
+
+ update: function update(value) {
+ if (!this.literal) {
+ this.setComponent(value);
+ }
+ },
+
+ /**
+ * 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;
+ } 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;
+ }
+ 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;
+ }
+ });
+ } 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);
+ }
+ },
+
+ /**
+ * 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();
+ }
+ 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);
+ }
+ }
+ }
+
+ 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;
+ }
+
+ // 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;
+ }
+
+ prop = {
+ name: name,
+ path: path,
+ options: options,
+ mode: propBindingModes.ONE_WAY,
+ raw: null
+ };
+
+ 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;
+ }
+ }
+ 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);
+ }
+ }
+ // 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);
+ }
+ }
+ };
+ }
+
+ /**
+ * 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);
+ });
+ } else {
+ fn(value);
+ }
+ }
+
+ /**
+ * Set a prop's initial value on a vm and its data object.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @param {*} value
+ */
+
+ function initProp(vm, prop, value) {
+ processPropValue(vm, prop, value, function (value) {
+ defineReactive(vm, prop.path, value);
+ });
+ }
+
+ /**
+ * Update a prop's value on a vm.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @param {*} value
+ */
+
+ function updateProp(vm, prop, value) {
+ processPropValue(vm, prop, value, function (value) {
+ vm[prop.path] = value;
+ });
+ }
+
+ /**
+ * Get the default value of a prop.
+ *
+ * @param {Vue} vm
+ * @param {Object} prop
+ * @return {*}
+ */
+
+ 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 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;
+ }
+ }
+ 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;
+ }
+ }
+ 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;
+ }
+ if (typeof coerce === 'function') {
+ return coerce(value);
+ } else {
+ 'development' !== 'production' && warn('Invalid coerce for prop "' + prop.name + '": expected function, got ' + typeof coerce + '.', vm);
+ return 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 {
+ valid = value instanceof type;
+ }
+ 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';
+ }
+
+ /**
+ * Format value
+ *
+ * @param {*} value
+ * @return {String}
+ */
+
+ function formatValue(val) {
+ return Object.prototype.toString.call(val).slice(8, -1);
+ }
+
+ var bindingModes = config._propBindingModes;
+
+ var propDef = {
+
+ 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);
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ var TYPE_TRANSITION = 'transition';
+ var TYPE_ANIMATION = 'animation';
+ var transDurationProp = transitionProp + 'Duration';
+ var animDurationProp = animationProp + 'Duration';
+
+ /**
+ * 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);
+ };
+
+ /**
+ * 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);
+ });
+ }
+
+ 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();
+ }
+ } else if (type === TYPE_TRANSITION) {
+ removeClass(this.el, this.enterClass);
+ }
+ };
+
+ /**
+ * The "cleanup" phase of an entering transition.
+ */
+
+ 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]
+ */
+
+ 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);
+ }
+ }
+ };
+
+ /**
+ * The "nextTick" phase of a leaving transition.
+ */
+
+ 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();
+ }
+ };
+
+ /**
+ * The "cleanup" phase of a leaving transition.
+ */
+
+ 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;
+ };
+
+ /**
+ * Cancel any pending callbacks from a previously running
+ * but not finished transition.
+ */
+
+ 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;
+ }
+ 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);
+ }
+ };
+
+ /**
+ * 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);
+ }
+ };
+
+ /**
+ * 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;
+ } else {
+ var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp];
+ if (animDuration && animDuration !== '0s') {
+ type = TYPE_ANIMATION;
+ }
+ }
+ if (type) {
+ this.typeCache[className] = type;
+ }
+ 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);
+ } else {
+ return !(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
+ }
+ }
+
+ var transition$1 = {
+
+ priority: TRANSITION,
+
+ 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');
+ }
+ };
+
+ var internalDirectives = {
+ style: style,
+ 'class': vClass,
+ component: component,
+ prop: propDef,
+ transition: transition$1
+ };
+
+ // 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();
+ }
+ 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;
+ }
+
+ /**
+ * 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}
+ */
+
+ function makeUnlinkFn(vm, dirs, context, contextDirs) {
+ function unlink(destroying) {
+ teardownDirs(vm, dirs, destroying);
+ if (context && contextDirs) {
+ teardownDirs(context, contextDirs);
+ }
+ }
+ // expose linked directives
+ unlink.dirs = dirs;
+ return unlink;
+ }
+
+ /**
+ * 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]);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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);
+ }
+
+ // link self
+ var selfDirs = linkAndCapture(function () {
+ if (replacerLinkFn) replacerLinkFn(vm, el);
+ }, vm);
+
+ // return the unlink function that tearsdown context
+ // container directives.
+ return makeUnlinkFn(vm, selfDirs, context, contextDirs);
+ };
+ }
+
+ /**
+ * Compile a node and return a nodeLinkFn based on the
+ * node type.
+ *
+ * @param {Node} node
+ * @param {Object} options
+ * @return {Function|null}
+ */
+
+ 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);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Compile an element and return a nodeLinkFn.
+ *
+ * @param {Element} el
+ * @param {Object} options
+ * @return {Function|null}
+ */
+
+ 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);
+ }
+ // check element directives
+ if (!linkFn) {
+ linkFn = checkElementDirectives(el, options);
+ }
+ // check component
+ if (!linkFn) {
+ linkFn = checkComponent(el, options);
+ }
+ // normal directives
+ if (!linkFn && hasAttrs) {
+ linkFn = compileDirectives(attrs, options);
+ }
+ 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);
+ }
+ return makeTextNodeLinkFn(tokens, frag, options);
+ }
+
+ /**
+ * Linker for an skipped text node.
+ *
+ * @param {Vue} vm
+ * @param {Text} node
+ */
+
+ function removeText(vm, node) {
+ remove(node);
+ }
+
+ /**
+ * Process a single text token.
+ *
+ * @param {Object} token
+ * @param {Object} options
+ * @return {Node}
+ */
+
+ function processTextToken(token, options) {
+ var el;
+ if (token.oneTime) {
+ el = document.createTextNode(token.value);
+ } 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
+ };
+ }
+ 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);
+ };
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Make a child link function for a node's childNodes.
+ *
+ * @param {Array<Function>} linkFns
+ * @return {Function} childLinkFn
+ */
+
+ 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);
+ }
+ }
+ };
+ }
+
+ /**
+ * Check for element directives (custom elements that should
+ * be resovled as terminal directives).
+ *
+ * @param {Element} el
+ * @param {Object} options
+ */
+
+ function checkElementDirectives(el, options) {
+ var tag = el.tagName.toLowerCase();
+ if (commonTagRE.test(tag)) {
+ return;
+ }
+ var def = resolveAsset(options, 'elementDirectives', tag);
+ if (def) {
+ return makeTerminalNodeLinkFn(el, tag, '', options, def);
+ }
+ }
+
+ /**
+ * 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);
+ };
+ 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];
+ }
+ }
+ }
+ }
+
+ if (termDef) {
+ return makeTerminalNodeLinkFn(el, dirName, value, options, termDef, rawName, arg, modifiers);
+ }
+ }
+
+ function skip() {}
+ skip.terminal = true;
+
+ /**
+ * 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 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;
+ }
+
+ /**
+ * Compile the directives on an element and return a linker.
+ *
+ * @param {Array|NamedNodeMap} attrs
+ * @param {Object} options
+ * @return {Function}
+ */
+
+ 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
+ });
+ }
+
+ if (dirs.length) {
+ return makeNodeLinkFn(dirs);
+ }
+ }
+
+ /**
+ * Parse modifiers from directive attribute name.
+ *
+ * @param {String} name
+ * @return {Object}
+ */
+
+ 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;
+ }
+ }
+ return res;
+ }
+
+ /**
+ * 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);
+ }
+ };
+ }
+
+ /**
+ * Check if an interpolation string contains one-time tokens.
+ *
+ * @param {Array} tokens
+ * @return {Boolean}
+ */
+
+ function hasOneTime(tokens) {
+ var i = tokens.length;
+ while (i--) {
+ if (tokens[i].oneTime) return true;
+ }
+ }
+
+ function isScript(el) {
+ return el.tagName === 'SCRIPT' && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript');
+ }
+
+ var specialCharRE = /[^\w\-:\.]/;
+
+ /**
+ * 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);
+ }
+ // 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);
+ }
+ }
+ 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));
+ }
+ 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}
+ */
+
+ 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);
+ }
+ }
+
+ /**
+ * Helper to extract a component container's attributes
+ * into a plain object array.
+ *
+ * @param {Element} el
+ * @return {Array}
+ */
+
+ function extractAttrs(el) {
+ if (el.nodeType === 1 && el.hasAttributes()) {
+ return toArray(el.attributes);
+ }
+ }
+
+ /**
+ * 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);
+ });
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+ /* eslint-enable no-cond-assign */
+ if ('development' !== 'production' && getBindAttr(el, 'slot')) {
+ warn('The "slot" attribute must be static.', vm.$parent);
+ }
+ }
+ 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;
+ }
+ contents['default'] = extractFragment(content.childNodes, content);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ return frag;
+ }
+
+
+
+ var compiler = Object.freeze({
+ compile: compile,
+ compileAndLinkProps: compileAndLinkProps,
+ compileRoot: compileRoot,
+ transclude: transclude,
+ resolveSlots: resolveSlots
+ });
+
+ 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();
+ };
+
+ /**
+ * 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;
+ };
+
+ /**
+ * 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);
+ };
+
+ /**
+ * 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();
+ };
+
+ /**
+ * 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;
+ }
+ });
+ }
+ };
+
+ /**
+ * 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
+ }
+ };
+
+ /**
+ * 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;
+ }
+ 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();
+ }
+ 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);
+ }
+ }
+ };
+
+ /**
+ * 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]);
+ }
+ }
+ };
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * Setup recursive attached/detached calls
+ */
+
+ Vue.prototype._initDOMHooks = function () {
+ this.$on('hook:attached', onAttached);
+ this.$on('hook:detached', onDetached);
+ };
+
+ /**
+ * Callback to recursively call attached hook on children
+ */
+
+ function onAttached() {
+ if (!this._isAttached) {
+ this._isAttached = true;
+ this.$children.forEach(callAttach);
+ }
+ }
+
+ /**
+ * Iterator to call attached hook
+ *
+ * @param {Vue} child
+ */
+
+ function callAttach(child) {
+ if (!child._isAttached && inDoc(child.$el)) {
+ child._callHook('attached');
+ }
+ }
+
+ /**
+ * Callback to recursively call detached hook on children
+ */
+
+ function onDetached() {
+ if (this._isAttached) {
+ this._isAttached = false;
+ this.$children.forEach(callDetach);
+ }
+ }
+
+ /**
+ * Iterator to call detached hook
+ *
+ * @param {Vue} child
+ */
+
+ function callDetach(child) {
+ if (child._isAttached && !inDoc(child.$el)) {
+ child._callHook('detached');
+ }
+ }
+
+ /**
+ * Trigger all handlers for a hook
+ *
+ * @param {String} hook
+ */
+
+ 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);
+ }
+ }
+ this.$emit('hook:' + hook);
+ };
+ }
+
+ function noop$1() {}
+
+ /**
+ * 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);
+ }
+ }
+
+ /**
+ * 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;
+
+ // 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;
+ } else {
+ extend(this, def);
+ }
+
+ // setup directive params
+ this._setupParams();
+
+ // initial bind
+ if (this.bind) {
+ this.bind();
+ }
+ this._bound = true;
+
+ 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;
+ }
+ 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);
+ }
+ }
+ };
+
+ /**
+ * 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;
+ }
+ }
+ }
+ };
+
+ /**
+ * 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);
+ }
+ } else {
+ called = true;
+ }
+ }, {
+ 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);
+ }
+ 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]);
+ };
+
+ /**
+ * Teardown the watcher and call unbind.
+ */
+
+ 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;
+ }
+ };
+
+ 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;
+ }
+ }
+ };
+
+ /**
+ * 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);
+ }
+ }
+
+ // 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);
+ };
+
+ // finally replace original
+ if (options.replace) {
+ replace(original, el);
+ }
+
+ 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;
+ }
+ 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;
+ }
+
+ 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();
+ }
+ };
+
+ // 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();
+ }
+ // 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;
+ }
+ }
+ 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);
+ } 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 : ''));
+ });
+ }
+ } 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;
+ };
+ } else {
+ try {
+ return res.get.call(this, this);
+ } catch (e) {}
+ }
+ }
+ };
+
+ /**
+ * 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);
+ }
+ };
+
+ /**
+ * 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);
+ }
+ 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);
+ }
+ };
+
+ /**
+ * 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;
+ }
+ };
+
+ /**
+ * 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]);
+ }
+ }
+ }
+ 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();
+ };
+ 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();
+ }
+
+ /**
+ * InsertBefore operation that takes a callback.
+ *
+ * @param {Node} el
+ * @param {Node} target
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
+ */
+
+ function beforeWithCb(el, target, vm, cb) {
+ before(el, target);
+ if (cb) cb();
+ }
+
+ /**
+ * Remove operation that takes a callback.
+ *
+ * @param {Node} el
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
+ */
+
+ function removeWithCb(el, vm, cb) {
+ remove(el);
+ if (cb) cb();
+ }
+ }
+
+ 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;
+ };
+
+ /**
+ * 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;
+ }
+ }
+ 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;
+ }
+ 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;
+ }
+ }
+ }
+ 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);
+ }
+ }
+ 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');
+ }
+
+ /**
+ * 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);
+ }
+
+ // 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);
+ } else {
+ remove(this.el);
+ }
+ },
+
+ fallback: function fallback() {
+ this.compile(extractContent(this.el, true), this.vm);
+ },
+
+ unbind: function unbind() {
+ if (this.unlink) {
+ this.unlink();
+ }
+ }
+ };
+
+ 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);
+ }
+ }
+ },
+
+ bind: function bind() {
+ this.anchor = createAnchor('v-partial');
+ replace(this.el, this.anchor);
+ this.insert(this.params.name);
+ },
+
+ 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);
+ }
+ },
+
+ unbind: function unbind() {
+ if (this.frag) {
+ this.frag.destroy();
+ }
+ }
+ };
+
+ var elementDirectives = {
+ slot: slot,
+ partial: partial
+ };
+
+ 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;
+ }
+
+ /**
+ * 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;
+ }
+ }
+ } else if (contains(item, search)) {
+ res.push(item);
+ }
+ }
+ return res;
+ }
+
+ /**
+ * 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);
+
+ // 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;
+ }
+
+ // 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;
+ };
+ } 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;
+ }
+ 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 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'
+ */
+
+ uppercase: function uppercase(value) {
+ return value || value === 0 ? value.toString().toUpperCase() : '';
+ },
+
+ /**
+ * 'AbC' => 'abc'
+ */
+
+ lowercase: function lowercase(value) {
+ return value || value === 0 ? value.toString().toLowerCase() : '';
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * '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];
+ } 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;
+ }
+ }
+ 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 */
+ }
+
+ /**
+ * 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);
+ }
+
+ installGlobalAPI(Vue);
+
+ Vue.version = '1.0.26';
+
+ // 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);
+
+ return Vue;
+
+})); \ No newline at end of file
diff --git a/vendor/assets/javascripts/vue.js.erb b/vendor/assets/javascripts/vue.js.erb
new file mode 100644
index 00000000000..008beb10f4d
--- /dev/null
+++ b/vendor/assets/javascripts/vue.js.erb
@@ -0,0 +1,2 @@
+<% type = Rails.env.development? ? 'full' : 'min' %>
+<%= File.read(Rails.root.join("vendor/assets/javascripts/vue.#{type}.js")) %>
diff --git a/vendor/assets/javascripts/vue.min.js b/vendor/assets/javascripts/vue.min.js
new file mode 100644
index 00000000000..2c9a8a0e117
--- /dev/null
+++ b/vendor/assets/javascripts/vue.min.js
@@ -0,0 +1,9 @@
+/*!
+ * Vue.js v1.0.26
+ * (c) 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
diff --git a/vendor/gitignore/Elm.gitignore b/vendor/gitignore/Elm.gitignore
index a594364e2c0..8b631e7de00 100644
--- a/vendor/gitignore/Elm.gitignore
+++ b/vendor/gitignore/Elm.gitignore
@@ -1,4 +1,4 @@
# elm-package generated files
-elm-stuff/
+elm-stuff
# elm-repl generated files
repl-temp-*
diff --git a/vendor/gitignore/Global/VisualStudioCode.gitignore b/vendor/gitignore/Global/VisualStudioCode.gitignore
index faa18382a3c..d9960081c98 100644
--- a/vendor/gitignore/Global/VisualStudioCode.gitignore
+++ b/vendor/gitignore/Global/VisualStudioCode.gitignore
@@ -1,2 +1,4 @@
-.vscode
-
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index daf913b1b34..cd0d5d1e2f4 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -22,3 +22,6 @@ _testmain.go
*.exe
*.test
*.prof
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/vendor/gitignore/Leiningen.gitignore b/vendor/gitignore/Leiningen.gitignore
index 47fed6c20d9..a9fe6fba80d 100644
--- a/vendor/gitignore/Leiningen.gitignore
+++ b/vendor/gitignore/Leiningen.gitignore
@@ -1,6 +1,7 @@
pom.xml
pom.xml.asc
-*jar
+*.jar
+*.class
/lib/
/classes/
/target/
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 86f21d8e0ff..20592083931 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -52,7 +52,7 @@ Carthage/Build
fastlane/report.xml
fastlane/screenshots
-#Code Injection
+# Code Injection
#
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index c58d83b3189..a02d882cb88 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -15,3 +15,7 @@ project/plugins/project/
# Scala-IDE specific
.scala_dependencies
.worksheet
+
+# ENSIME specific
+.ensime_cache/
+.ensime
diff --git a/vendor/gitignore/SugarCRM.gitignore b/vendor/gitignore/SugarCRM.gitignore
index 842c3ec518b..e9270205fd5 100644
--- a/vendor/gitignore/SugarCRM.gitignore
+++ b/vendor/gitignore/SugarCRM.gitignore
@@ -7,6 +7,7 @@
# For development the cache directory can be safely ignored and
# therefore it is ignored.
/cache/
+!/cache/index.html
# Ignore some files and directories from the custom directory.
/custom/history/
/custom/modulebuilder/
@@ -22,4 +23,5 @@
*.log
# Ignore the new upload directories.
/upload/
+!/upload/index.html
/upload_backup/
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 3cb097c9d5e..34f999df3e7 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -19,6 +19,9 @@
# *.eps
# *.pdf
+## Generated if empty string is given at "Please type another file name for output:"
+.pdf
+
## Bibliography auxiliary files (bibtex/biblatex/biber):
*.bbl
*.bcf
@@ -31,6 +34,7 @@
## Build tool auxiliary files:
*.fdb_latexmk
*.synctex
+*.synctex(busy)
*.synctex.gz
*.synctex.gz(busy)
*.pdfsync
@@ -84,6 +88,10 @@ acs-*.bib
# gnuplottex
*-gnuplottex-*
+# gregoriotex
+*.gaux
+*.gtex
+
# hyperref
*.brf
@@ -128,6 +136,9 @@ _minted*
*.sagetex.py
*.sagetex.scmd
+# scrwfile
+*.wrt
+
# sympy
*.sout
*.sympy
diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore
index 7868d16d216..41859c81f1c 100644
--- a/vendor/gitignore/Terraform.gitignore
+++ b/vendor/gitignore/Terraform.gitignore
@@ -1,3 +1,6 @@
# Compiled files
*.tfstate
*.tfstate.backup
+
+# Module directory
+.terraform/
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 5aafcbb7f1d..1c10388911b 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -5,8 +5,9 @@
/[Bb]uilds/
/Assets/AssetStoreTools*
-# Autogenerated VS/MD solution and project files
+# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
+.consulo/
*.csproj
*.unityproj
*.sln
diff --git a/vendor/gitlab-ci-yml/C++.gitlab-ci.yml b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml
new file mode 100644
index 00000000000..c83c49d8c95
--- /dev/null
+++ b/vendor/gitlab-ci-yml/C++.gitlab-ci.yml
@@ -0,0 +1,26 @@
+# use the official gcc image, based on debian
+# can use verions as well, like gcc:5.2
+# see https://hub.docker.com/_/gcc/
+image: gcc
+
+build:
+ stage: build
+ # instead of calling g++ directly you can also use some build toolkit like make
+ # install the necessary build tools when needed
+ # before_script:
+ # - apt update && apt -y install make autoconf
+ script:
+ - g++ helloworld.cpp -o mybinary
+ artifacts:
+ paths:
+ - mybinary
+ # depending on your build setup it's most likely a good idea to cache outputs to reduce the build time
+ # cache:
+ # paths:
+ # - "*.o"
+
+# run tests using the binary built before
+test:
+ stage: test
+ script:
+ - ./runmytests.sh
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
index 0b329aaf1c4..00f9541e89b 100644
--- a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -2,7 +2,7 @@
# The image already has Hex installed. You might want to consider to use `elixir:latest`
image: trenpixster/elixir:latest
-# Pic zero or more services to be used on all builds.
+# Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
services:
diff --git a/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml
new file mode 100644
index 00000000000..7fc698d50cf
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Grails.gitlab-ci.yml
@@ -0,0 +1,40 @@
+# This template uses the java:8 docker image because there isn't any
+# official Grails image at this moment
+#
+# Grails Framework https://grails.org/ is a powerful Groovy-based web application framework for the JVM
+#
+# This yml works with Grails 3.x only
+# Feel free to change GRAILS_VERSION version with your project version (3.0.1, 3.1.1,...)
+# Feel free to change GRADLE_VERSION version with your gradle project version (2.13, 2.14,...)
+# If you use Angular profile, this yml it's prepared to work with it
+
+image: java:8
+
+variables:
+ GRAILS_VERSION: "3.1.9"
+ GRADLE_VERSION: "2.13"
+
+# We use SDKMan as tool for managing versions
+before_script:
+ - apt-get update -qq && apt-get install -y -qq unzip
+ - curl -sSL https://get.sdkman.io | bash
+ - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+ - source /root/.sdkman/bin/sdkman-init.sh
+ - sdk install gradle $GRADLE_VERSION < /dev/null
+ - sdk use gradle $GRADLE_VERSION
+# As it's not a good idea to version gradle.properties feel free to add your
+# environments variable here
+ - echo grailsVersion=$GRAILS_VERSION > gradle.properties
+ - echo gradleWrapperVersion=2.14 >> gradle.properties
+# refresh dependencies from your project
+ - ./gradlew --refresh-dependencies
+# Be aware that if you are using Angular profile,
+# Bower cannot be run as root if you don't allow it before.
+# Feel free to remove next line if you are not using Bower
+ - echo {\"allow_root\":true} > /root/.bowerrc
+
+# This build job does the full grails pipeline
+# (compile, test, integrationTest, war, assemble).
+build:
+ script:
+ - ./gradlew build \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml
new file mode 100644
index 00000000000..a4aed36889e
--- /dev/null
+++ b/vendor/gitlab-ci-yml/LaTeX.gitlab-ci.yml
@@ -0,0 +1,11 @@
+# use docker image with latex preinstalled
+# since there is no official latex image, use https://github.com/blang/latex-docker
+# possible alternative: https://github.com/natlownes/docker-latex
+image: blang/latex
+
+build:
+ script:
+ - latexmk -pdf
+ artifacts:
+ paths:
+ - "*.pdf"
diff --git a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
index b468d79bcad..908463c9d12 100644
--- a/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hexo.gitlab-ci.yml
@@ -1,25 +1,17 @@
# Full project: https://gitlab.com/pages/hexo
-image: python:2.7
-
-cache:
- paths:
- - vendor/
-
-test:
- stage: test
- script:
- - pip install hyde
- - hyde gen
- except:
- - master
+image: node:4.2.2
pages:
- stage: deploy
+ cache:
+ paths:
+ - node_modules/
+
script:
- - pip install hyde
- - hyde gen -d public
+ - npm install hexo-cli -g
+ - npm install
+ - hexo deploy
artifacts:
paths:
- public
only:
- - master
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml
new file mode 100644
index 00000000000..bc36a4e6966
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/JBake.gitlab-ci.yml
@@ -0,0 +1,32 @@
+# This template uses the java:8 docker image because there isn't any
+# official JBake image at this moment
+#
+# JBake https://jbake.org/ is a Java based, open source, static site/blog generator for developers & designers
+#
+# This yml works with jBake 2.4.0
+# Feel free to change JBAKE_VERSION version
+#
+# HowTo at: https://jorge.aguilera.gitlab.io/howtojbake/
+
+image: java:8
+
+variables:
+ JBAKE_VERSION: 2.4.0
+
+
+# We use SDKMan as tool for managing versions
+before_script:
+ - apt-get update -qq && apt-get install -y -qq unzip
+ - curl -sSL https://get.sdkman.io | bash
+ - echo sdkman_auto_answer=true > /root/.sdkman/etc/config
+ - source /root/.sdkman/bin/sdkman-init.sh
+ - sdk install jbake $JBAKE_VERSION < /dev/null
+ - sdk use jbake $JBAKE_VERSION
+
+# This build job produced the output directory of your site
+pages:
+ script:
+ - jbake . public
+ artifacts:
+ paths:
+ - public \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
index 2a761bbd127..166f146ee05 100644
--- a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -10,6 +10,9 @@ services:
- redis:latest
- postgres:latest
+variables:
+ POSTGRES_DB: database_name
+
# Cache gems in between builds
cache:
paths:
@@ -19,6 +22,8 @@ cache:
# services such as redis or postgres
before_script:
- ruby -v # Print out ruby version for debugging
+ # Uncomment next line if your rails app needs a JS runtime:
+ # - apt-get update -q && apt-get install nodejs -yqq
- gem install bundler --no-ri --no-rdoc # Bundler is not installed with the image
- bundle install -j $(nproc) --path vendor # Install dependencies into ./vendor/ruby
@@ -32,6 +37,9 @@ rspec:
- rspec spec
rails:
+ variables:
+ DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
script:
- bundle exec rake db:migrate
+ - bundle exec rake db:seed
- bundle exec rake test